From 913166c809d704e7204986fd59f3ee62030f8fcf Mon Sep 17 00:00:00 2001 From: ibmmqmet Date: Mon, 23 Oct 2023 09:05:54 +0100 Subject: [PATCH] Update for MQ 9.3.4 - Uniform Cluster balancing options now available in JMS Fix SSLBundle sequencing (#96) --- CHANGES.md | 8 + README.md | 175 +++++----- RUNME.sh | 2 +- build.gradle | 6 +- gradle/wrapper/gradle-wrapper.jar | Bin 55616 -> 59821 bytes gradle/wrapper/gradle-wrapper.properties | 2 +- gradlew | 260 +++++++++------ gradlew.bat | 25 +- jms2.properties | 8 +- jms3.properties | 8 +- mq-jms-spring-boot-starter/build.gradle | 7 +- .../mq/spring/boot/MQAutoConfiguration.java | 14 +- .../boot/MQConfigurationProperties.java | 308 ++++++++++++++---- .../boot/MQConfigurationSslBundles.java | 41 ++- .../boot/MQConnectionFactoryFactory.java | 38 ++- ...ot.autoconfigure.AutoConfiguration.imports | 2 +- samples/s1/build.gradle | 9 +- samples/s2.tls.jms3/build.gradle | 8 +- .../src/main/java/sample2/Application.java | 6 +- .../src/main/resources/application.yml | 10 +- samples/s2.tls/build.gradle | 6 +- samples/s2/build.gradle | 9 +- samples/s3.jms3/build.gradle | 6 +- samples/s3/build.gradle | 7 +- 24 files changed, 603 insertions(+), 362 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 7cebf67..72a3b31 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,6 +1,14 @@ # Changelog Newest updates are at the top of this file +## 2.7.17 and 3.1.5 (2023-10-20) +- Update Spring dependencies +- Update to MQ 9.3.4.0 + - Uniform Cluster balancing options now available in JMS +- Fix SSLBundle sequencing (#96) +- Update documentation links to reflect IBM site changes (#100) +- Update to Gradle 8.4 + ## 2.7.14 and 3.1.2 (2023-07-20) - Update Spring dependencies - Add SSLBundle configuration option for Boot 3.1 apps (#94) diff --git a/README.md b/README.md index 3f8f871..bd88be5 100644 --- a/README.md +++ b/README.md @@ -10,15 +10,18 @@ The library contains: The compiled versions of this package can be automatically downloaded from Maven Central. -For local modifications -and building it yourself, you can use the `RUNME.sh` script. It uses gradle as the build mechanism and has tasks that can -push compiled jars to either a local repository (typically under `$HOME/.m2`) or to Maven Central. -When signing/authentication of modules is required, use the `gradle.properties.template` file as a starter for your own `gradle.properties`. +For local modifications and building it yourself, you can use the `RUNME.sh` script. It uses gradle as the build +mechanism and has tasks that can push compiled jars to either a local repository (typically under `$HOME/.m2`) or to +Maven Central. When signing/authentication of modules is required, use the `gradle.properties.template` file as a +starter for your own `gradle.properties`. -Java 17 is required as the compiler level when building this package, as that is the baseline for Spring 3. Compiler directives are used to build -the Spring 2 version as compatible with the older Java 8 runtime. +Java 17 is required as the compiler level when building this package, as that is the baseline for Spring 3. Compiler +directives are used to build the Spring 2 version as compatible with the older Java 8 runtime. -The script builds modules for both the JMS2 and JMS3 standards. The JMS3 (Jakarta) variant is the primary source. The older JMS2 version does not have a separate source tree in this repository. Instead, the source is generated automatically during the build process, by simply changing package names in the JMS3 code. The created jar files have the same names, but different version numbers. +The script builds modules for both the JMS2 and JMS3 standards. The JMS3 (Jakarta) variant is the primary source. The +older JMS2 version does not have a separate source tree in this repository. Instead, the source is generated +automatically during the build process, by simply changing package names in the JMS3 code. The created jar files have +the same names, but different version numbers. ### Spring Boot Applications @@ -43,8 +46,7 @@ Maven: ``` -**Note** This repository and the corresponding Maven Central artifacts require -either Spring Boot 2 or 3. +**Note** This repository and the corresponding Maven Central artifacts require either Spring Boot 2 or 3. ## Design Approach @@ -89,25 +91,25 @@ The default attributes are ### Connection security -The default userid and password have been chosen for a commonly-used queue manager -configuration. +The default userid and password have been chosen for a commonly-used queue manager configuration. -To disable user/password checking entirely, you must set the `ibm.mq.user` attribute to an empty value -so that the default is not used. +To disable user/password checking entirely, you must set the `ibm.mq.user` and `ibm.mq.password` attribute to empty +values so that the defaults are not used. ``` ibm.mq.user= + ibm.mq.password= ``` -Of course, that level of access must be permitted by your queue manager. The usual CHLAUTH and CONNAUTH -rules will apply to assign an identity to the connection. +Of course, that level of access must be permitted by your queue manager. The usual CHLAUTH and CONNAUTH rules will apply +to assign an identity to the connection. Configuration of secure connections with TLS are discussed below. ### Configuration Options -If you already have a running MQ queue manager that you want to use, then you can easily modify the -default configuration to match by providing override values. +If you already have a running MQ queue manager that you want to use, then you can easily modify the default +configuration to match by providing override values. The queue manager name is given as @@ -120,13 +122,12 @@ For client connections to a queue manager, you must also have either or - `ibm.mq.ccdtUrl` -If both the channel and connName are empty, and the CCDTURL is not supplied, -then a local queue manager is assumed. The CCDTURL property is taken in preference to -the channel and connName. The channel and connName have non-blank defaults, so must be -explicitly set to empty strings if you do not wish them to be used. +If both the channel and connName are empty, and the CCDTURL is not supplied, then a local queue manager is assumed. The +CCDTURL property is taken in preference to the channel and connName. The channel and connName have non-blank defaults, +so must be explicitly set to empty strings if you do not wish them to be used. -Optionally you can provide a [client id](https://www.ibm.com/support/knowledgecenter/en/SSFKSJ_latest/com.ibm.mq.ref.dev.doc/q112000_.html) -and [application name](https://www.ibm.com/support/knowledgecenter/en/SSFKSJ_latest/com.ibm.mq.ref.dev.doc/q111810_.htm) if required. +Optionally you can provide a [client id](https://www.ibm.com/docs/en/ibm-mq/latest?topic=objects-clientid) +and [application name](https://www.ibm.com/docs/en/ibm-mq/latest?topic=applications-specifying-application-name-in-supported-programming-languages) if required. - `ibm.mq.clientId` - `ibm.mq.applicationName` @@ -164,6 +165,9 @@ Spring Boot will then create a ConnectionFactory that can then be used to intera | ibm.mq.tempModel | The name of a model queue for creating temporary destinations. | | ibm.mq.reconnect | Whether app tries automatic reconnect. Options of YES/NO/QMGR/DISABLED/DEFAULT | | ibm.mq.autoConfigure | If explicitly set to "false", then the autoconfigure bean is disabled | +| ibm.mq.balancingApplicationType | Hint how uniform clusters should treat the app. Options of SIMPLE/REQREP | +| ibm.mq.balancingTimeout | Uniform cluster timer. Options NEVER/DEFAULT/IMMEDIATE or integer seconds | +| ibm.mq.balancingOptions | Rebalancing options. Options of NONE/IGNORETRANS. Default NONE. | The `reconnect` option was previously named `defaultReconnect` but both names work in the configuration. @@ -198,14 +202,14 @@ and These JKS options are an alternative to setting the `javax.net.ssl` system properties, usually done on the command line. -An alternative preferred approach for setting the key/truststores is -available from Spring 3.1, which introduced the concept of "SSL Bundles". This makes it possible to have different -SSL configurations - keystores, truststores etc - for different components executing in the same Spring-managed process. -See [here](https://spring.io/blog/2023/06/07/securing-spring-boot-applications-with-ssl) -for a description of the options available. Each bundle has an identifier with the `spring.ssl.bundle.jks.` tree of options. -The key can be specified for this package with `ibm.mq.sslBundle` which then uses the Spring elements to create the -connection configuration. The default value for this key is empty, meaning that `SSLBundles` will not be used; the global -SSL configuration is used instead. However the `ibm.mq.jks` properties are now marked as deprecated. +An alternative preferred approach for setting the key/truststores is available from Spring 3.1, which introduced the +concept of "SSL Bundles". This makes it possible to have different SSL configurations - keystores, truststores etc - for +different components executing in the same Spring-managed process. See +[here](https://spring.io/blog/2023/06/07/securing-spring-boot-applications-with-ssl) for a description of the options +available. Each bundle has an identifier with the `spring.ssl.bundle.jks.` tree of options. The key can be +specified for this package with `ibm.mq.sslBundle` which then uses the Spring elements to create the connection +configuration. The default value for this key is empty, meaning that `SSLBundles` will not be used; the global SSL +configuration is used instead. However the `ibm.mq.jks` properties are now marked as deprecated. | Option | Description | | ------------------------------- | ---------------------------------------------------------------------------- | @@ -246,10 +250,10 @@ the options can be found [here](https://github.com/messaginghub/pooled-jms/blob/ ### JMS Polling Listener Timer configuration -The Spring AbstractPollingMessageListenerContainer interface has a default polling timer of 1 second. This can be configured -with the `spring.jms.listener.receiveTimeout` property. If the property is not explicitly set, then this MQ Spring Boot -component resets the initial timeout value to 30 seconds which has been shown to be more cost-effective. Application code -can still set its own preferred value. +The Spring AbstractPollingMessageListenerContainer interface has a default polling timer of 1 second. This can be +configured with the `spring.jms.listener.receiveTimeout` property. If the property is not explicitly set, then this MQ +Spring Boot component resets the initial timeout value to 30 seconds which has been shown to be more cost-effective. +Application code can still set its own preferred value. | Option | Description | | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- | @@ -257,53 +261,50 @@ can still set its own preferred value. ### Additional properties -Additional properties that are not in the recognised sets listed here can be put onto the -Connection Factory via a map in the external properties definitions. Use the format -`ibm.mq.additionalProperties.CONSTANT_NAME=value`. The CONSTANT_NAME can be either the -real string for the property, and will often begin with "XMSC", or it can be the variable as known -in the WMQConstants class. +Additional properties that are not in the recognised sets listed here can be put onto the Connection Factory via a map +in the external properties definitions. Use the format `ibm.mq.additionalProperties.CONSTANT_NAME=value`. The +CONSTANT_NAME can be either the real string for the property, and will often begin with "XMSC", or it can be the +variable as known in the WMQConstants class. -For example, the constant `WMQConstants.WMQ_SECURITY_EXIT` has the value `"XMSC_WMQ_SECURITY_EXIT"` -and can be written in the properties file either as - `ibm.mq.additionalProperties.XMSC_WMQ_SECURITY_EXIT=com.example.SecExit` -or as - `ibm.mq.additionalProperties.WMQ_SECURITY_EXIT=com.example.SecExit` +For example, the constant `WMQConstants.WMQ_SECURITY_EXIT` has the value `"XMSC_WMQ_SECURITY_EXIT"` and can be written +in the properties file either as `ibm.mq.additionalProperties.XMSC_WMQ_SECURITY_EXIT=com.example.SecExit` or as +`ibm.mq.additionalProperties.WMQ_SECURITY_EXIT=com.example.SecExit` +There is no error checking on the property name or value. This may help with enabling rarely-used properties and reduce +the need for a customizer method in application code. See +[the KnowledgeCenter](https://www.ibm.com/docs/en/ibm-mq/latest?topic=messaging-mqconnectionfactoryconnectionfactoryproperty) +for a list of all the currently-recognised properties that may be set on a CF - though note that many are now +deprecated. -There is no error checking on the property name or value. This may help with enabling -rarely-used properties and reduce the need for a customizer method in application -code. See [the KnowledgeCenter](https://www.ibm.com/support/knowledgecenter/en/SSFKSJ_latest/com.ibm.mq.javadoc.doc/WMQJMSClasses/com/ibm/mq/jms/MQConnectionFactory.ConnectionFactoryProperty.html) -for a list of all the currently-recognised properties that may be set on a CF - though note that many are now deprecated. - -If the value looks like a number, it is treated as such. You can use hex constants beginning "0X" or decimals for a number. -Similarly if the value is TRUE/FALSE then that is processed as a boolean. -So you cannot try to set a string property that appears to be an integer. -Symbols representing the value of integer attributes cannot be used - the real +If the value looks like a number, it is treated as such. You can use hex constants beginning "0X" or decimals for a +number. Similarly if the value is TRUE/FALSE then that is processed as a boolean. So you cannot try to set a string +property that appears to be an integer. Symbols representing the value of integer attributes cannot be used - the real number must be given. ## JNDI -Spring already has configuration parameters for the use of a JNDI repository with a JMS program. -See the [Spring documentation](https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/jms.html) for more -details. +Spring already has configuration parameters for the use of a JNDI repository with a JMS program. See the +[Spring documentation](https://docs.spring.io/spring-framework/docs/3.2.x/spring-framework-reference/html/jms.html) for +more details. -However this package also enables some simple use of JNDI for Connection definitions (but not Destinations, as they are still always -handled by the core Spring classes). +However this package also enables some simple use of JNDI for Connection definitions (but not Destinations, as they are +still always handled by the core Spring classes). -You can set the `ibm.mq.jndi.providerUrl` and `ibm.mq.jndi.providerContextFactory` attributes to define -how the lookup is to be carried out. For example, +You can set the `ibm.mq.jndi.providerUrl` and `ibm.mq.jndi.providerContextFactory` attributes to define how the lookup +is to be carried out. For example, ``` ibm.mq.jndi.providerUrl=file:///home/username/mqjms/jndi ibm.mq.jndi.providerContextFactory=com.sun.jndi.fscontext.RefFSContextFactory ``` -If you choose to use this mechanism, all of the other queue manager properties that might be defined in your resource definitions are ignored and not -traced in order to avoid confusion. They will instead be picked up from the ConnectionFactory definition in JNDI. -The `queueManager` property is then more accurately used as the ConnectionFactory name used as the lookup. If you are using -an LDAP JNDI provider, then the CF name will be modified if necessary to always begin with `cn=`. +If you choose to use this mechanism, all of the other queue manager properties that might be defined in your resource +definitions are ignored and not traced in order to avoid confusion. They will instead be picked up from the +ConnectionFactory definition in JNDI. The `queueManager` property is then more accurately used as the ConnectionFactory +name used as the lookup. If you are using an LDAP JNDI provider, then the CF name will be modified if necessary to +always begin with `cn=`. -The `ibm.mq.jndi.additionalProperties` prefix can be used for any other JNDI-related properties that need to be applied to the -*Context* object. The symbolic name of the field from that Java class can be used. For example, +The `ibm.mq.jndi.additionalProperties` prefix can be used for any other JNDI-related properties that need to be applied +to the *Context* object. The symbolic name of the field from that Java class can be used. For example, ``` ibm.mq.jndi.additionalProperties.SECURITY_CREDENTIALS=passw0rd @@ -314,29 +315,27 @@ results in env.put(Context.SECURITY_CREDENTIALS,"passw0rd") ``` -The `MQConnectionFactoryFactory.getJndiContext` method is public so you can use it with your own -constructed properties object and get access to a JNDI Context object - it might make it easier to work -with Destinations if you can reuse the same way of getting directory access. +The `MQConnectionFactoryFactory.getJndiContext` method is public so you can use it with your own constructed properties +object and get access to a JNDI Context object - it might make it easier to work with Destinations if you can reuse the +same way of getting directory access. ## Logging & Tracing -The package makes use of the logging capabilities within Spring. You can enable -tracing of this specific component in your application's properties file by setting -`logging.level.com.ibm.mq.spring.boot=TRACE`. Otherwise it uses the standard -inheritance of logging configuration from `logging.level.root`downwards. +The package makes use of the logging capabilities within Spring. You can enable tracing of this specific component in +your application's properties file by setting `logging.level.com.ibm.mq.spring.boot=TRACE`. Otherwise it uses the +standard inheritance of logging configuration from `logging.level.root`downwards. ## Related documentation -- [MQ documentation](https://www.ibm.com/support/knowledgecenter/en/SSFKSJ_latest) +- [MQ documentation](https://www.ibm.com/docs/en/ibm-mq/latest) - [Spring Boot documentation](https://projects.spring.io/spring-boot/) - [Spring Framework documentation](https://projects.spring.io/spring-framework/) ### Contributions and Pull requests -Contributions to this package can be accepted under the terms of the Developer's Certificate -of Origin, found in the [DCO file](DCO1.1.txt) of this repository. When -submitting a pull request, you must include a statement stating you accept the terms -in the DCO. +Contributions to this package can be accepted under the terms of the Developer's Certificate of Origin, found in the +[DCO file](DCO1.1.txt) of this repository. When submitting a pull request, you must include a statement stating you +accept the terms in the DCO. ### Using in Other Projects @@ -346,20 +345,20 @@ The preferred approach for using this package in other projects will be to use t Copyright © 2018, 2023 IBM Corp. All rights reserved. -Licensed under the apache license, version 2.0 (the "license"); you may not use this file except in compliance with the license. -You may obtain a copy of the license at +Licensed under the apache license, version 2.0 (the "license"); you may not use this file except in compliance with the +license. You may obtain a copy of the license at http://www.apache.org/licenses/LICENSE-2.0.html -Unless required by applicable law or agreed to in writing, software distributed under the license is distributed on an "as is" basis, -without warranties or conditions of any kind, either express or implied. See the license for the specific language governing -permissions and limitations under the license. +Unless required by applicable law or agreed to in writing, software distributed under the license is distributed on an +"as is" basis, without warranties or conditions of any kind, either express or implied. See the license for the specific +language governing permissions and limitations under the license. ### Health Warning -This package is provided as-is with no guarantees of support or updates. You cannot use -IBM formal support channels (Cases/PMRs) for assistance with material in this repository. -There are also no guarantees of compatibility with any future versions of the package; -the API is subject to change based on any feedback. Versioned releases are made to assist with using stable APIs. +This package is provided as-is with no guarantees of support or updates. You cannot use IBM formal support channels +(Cases/PMRs) for assistance with material in this repository. There are also no guarantees of compatibility with any +future versions of the package; the API is subject to change based on any feedback. Versioned releases are made to +assist with using stable APIs. ### Issues diff --git a/RUNME.sh b/RUNME.sh index 3a5c81a..7e315cb 100755 --- a/RUNME.sh +++ b/RUNME.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Copyright 2022 IBM Corp. All rights reserved. +# Copyright 2022, 2023 IBM Corp. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of the License at diff --git a/build.gradle b/build.gradle index 1e2c99f..6705c8a 100644 --- a/build.gradle +++ b/build.gradle @@ -37,7 +37,7 @@ subprojects { apply plugin: 'signing' // This is the MQ client version - ext.mqVersion = '9.3.3.0' + ext.mqVersion = '9.3.4.0' ext.mqGroup = 'com.ibm.mq' // The groupid for the compiled jars when uploaded @@ -92,7 +92,7 @@ subprojects { } task sourcesJar(type: Jar, dependsOn: ['mqPrereq',classes]) { - classifier = 'sources' + archiveClassifier = 'sources' sourceSets.all { into(name + "/java", { from allJava }) into(name + "/resources", { from resources }) @@ -125,7 +125,7 @@ subprojects { } task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' + archiveClassifier = 'javadoc' from javadoc.destinationDir } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 5c2d1cf016b3885f6930543d57b744ea8c220a1a..41d9927a4d4fb3f96a785543079b8df6723c946b 100644 GIT binary patch delta 26784 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

_` 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 41dfb87..e411586 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,5 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.4-bin.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index 83f2acf..1b6c787 100755 --- a/gradlew +++ b/gradlew @@ -1,7 +1,7 @@ -#!/usr/bin/env sh +#!/bin/sh # -# Copyright 2015 the original author or authors. +# Copyright © 2015-2021 the original authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. @@ -17,78 +17,113 @@ # ############################################################################## -## -## Gradle start up script for UN*X -## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# ############################################################################## # Attempt to set APP_HOME + # Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` +APP_BASE_NAME=${0##*/} # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" +MAX_FD=maximum warn () { echo "$*" -} +} >&2 die () { echo echo "$*" echo exit 1 -} +} >&2 # OS specific support (must be 'true' or 'false'). cygwin=false msys=false darwin=false nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" + JAVACMD=$JAVA_HOME/jre/sh/java else - JAVACMD="$JAVA_HOME/bin/java" + JAVACMD=$JAVA_HOME/bin/java fi if [ ! -x "$JAVACMD" ] ; then die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME @@ -97,7 +132,7 @@ Please set the JAVA_HOME variable in your environment to match the location of your Java installation." fi else - JAVACMD="java" + JAVACMD=java which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. Please set the JAVA_HOME variable in your environment to match the @@ -105,84 +140,95 @@ location of your Java installation." fi # Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac fi -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. # For Cygwin or MSYS, switch paths to Windows format before running java -if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) fi - i=$((i+1)) + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac fi -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 24467a1..ac1b06f 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -29,6 +29,9 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @@ -37,7 +40,7 @@ if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -51,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -61,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/jms2.properties b/jms2.properties index 8f5cd1b..09e8241 100644 --- a/jms2.properties +++ b/jms2.properties @@ -1,14 +1,14 @@ // This file contains the versions of Spring etc to work with a javax.jms-based system ext { // Our shipped version - should usually match the Spring Boot Version - mqStarterVersion = '2.7.14' + mqStarterVersion = '2.7.17' // Direct Dependencies - give versions here - springVersion = '5.3.29' - springBootVersion = '2.7.14' + springVersion = '5.3.30' + springBootVersion = '2.7.17' // The pooledJms v2.x level is built against Java 11 so we can't move there - pooledJmsVersion = '1.2.4' + pooledJmsVersion = '1.2.5' jUnitVersion = '4.13.2' // MQ client package diff --git a/jms3.properties b/jms3.properties index 41a9588..ce61434 100644 --- a/jms3.properties +++ b/jms3.properties @@ -2,13 +2,13 @@ ext { // Our shipped version - should usually match the Spring Boot Version but // we keep it different during the pre-GA releases - mqStarterVersion = '3.1.2' + mqStarterVersion = '3.1.5' // Direct Dependencies - give versions here - springVersion = '6.0.11' - springBootVersion = '3.1.2' + springVersion = '6.0.13' + springBootVersion = '3.1.5' - pooledJmsVersion = '3.1.0' + pooledJmsVersion = '3.1.4' jUnitVersion = '4.13.2' // MQ client has a 'jakarta' name diff --git a/mq-jms-spring-boot-starter/build.gradle b/mq-jms-spring-boot-starter/build.gradle index 515947d..2cb0660 100644 --- a/mq-jms-spring-boot-starter/build.gradle +++ b/mq-jms-spring-boot-starter/build.gradle @@ -1,5 +1,5 @@ /* - * Copyright © 2018,2022 IBM Corp. All rights reserved. + * Copyright © 2018,2023 IBM Corp. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at @@ -14,10 +14,11 @@ buildscript { repositories { - maven { url 'https://repo.spring.io/plugins-release' } + // maven { url 'https://repo.spring.io/plugins-release' } + mavenCentral() } dependencies { - classpath 'io.spring.gradle:propdeps-plugin:0.0.9.RELEASE' + // classpath 'io.spring.gradle:propdeps-plugin:0.0.9.RELEASE' } } diff --git a/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQAutoConfiguration.java b/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQAutoConfiguration.java index f98bbbf..18e1ab8 100644 --- a/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQAutoConfiguration.java +++ b/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright © 2018, 2020 IBM Corp. All rights reserved. + * Copyright © 2018, 2023 IBM Corp. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at @@ -14,8 +14,8 @@ package com.ibm.mq.spring.boot; -import jakarta.jms.ConnectionFactory; - +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; @@ -31,6 +31,8 @@ import com.ibm.mq.jakarta.jms.MQConnectionFactory; +import jakarta.jms.ConnectionFactory; + // See https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-3.0.0-M5-Release-Notes // where autoconfiguration was moved from META-INF/spring.factories to a separate file. The // original file can remain in place though for both Boot 2 and Boot 3. @@ -42,7 +44,11 @@ @ConditionalOnProperty(prefix = "ibm.mq", name = "autoConfigure", matchIfMissing=true) @ConditionalOnMissingBean(ConnectionFactory.class) @EnableConfigurationProperties({MQConfigurationProperties.class, JmsProperties.class}) -@Import({ MQXAConnectionFactoryConfiguration.class,MQConnectionFactoryConfiguration.class }) +@Import({ MQConfigurationSslBundles.class, MQXAConnectionFactoryConfiguration.class,MQConnectionFactoryConfiguration.class }) public class MQAutoConfiguration { + private static Logger logger = LoggerFactory.getLogger(MQAutoConfiguration.class); + public MQAutoConfiguration() { + logger.trace("constructor"); + } } diff --git a/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQConfigurationProperties.java b/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQConfigurationProperties.java index 8cb9c40..4e2aa9b 100644 --- a/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQConfigurationProperties.java +++ b/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQConfigurationProperties.java @@ -14,6 +14,8 @@ package com.ibm.mq.spring.boot; +import java.time.Duration; +import java.time.format.DateTimeParseException; import java.util.HashMap; import java.util.Map; @@ -26,15 +28,19 @@ import com.ibm.msg.client.jakarta.wmq.WMQConstants; /** - * There are many properties that can be set on an MQ Connection Factory/ This class allows configuration for most of them - * for both direct and client connections. Any that are not explicitly named in here can be managed through the "additionalProperties" - * map. + * There are many properties that can be set on an MQ Connection Factory/ This + * class allows configuration for most of them + * for both direct and client connections. Any that are not explicitly named in + * here can be managed through the "additionalProperties" map. *

- * This class allows for setting the CipherSuite/CipherSpec property, and an indication of whether or not - * to use the IBM JRE maps for Cipher names - that's not something that is standardised. + * This class allows for setting the CipherSuite/CipherSpec property, and an + * indication of whether or not + * to use the IBM JRE maps for Cipher names - that's not something that is + * standardised. *

* The default values have been set to match the settings of the - * developer-configured container. + * developer-configured + * container. * *

    *
  • queueManager = QM1 @@ -49,11 +55,16 @@ public class MQConfigurationProperties { private static Logger logger = LoggerFactory.getLogger(MQConfigurationProperties.class); - // Some system properties that may be set through this package. They are not regular CF properties, but still - // affect how connections are made. + // Some system properties that may be set through this package. They are not + // regular CF properties, but still affect how connections are made. private static final String PROPERTY_USE_IBM_CIPHER_MAPPINGS = "com.ibm.mq.cfg.useIBMCipherMappings"; - private static final String PROPERTY_OUTBOUND_SNI = "com.ibm.mq.cfg.SSL.outboundSNI"; - private static final String PROPERTY_CHANNEL_SHARING = "com.ibm.mq.jms.channel.sharing"; + private static final String PROPERTY_OUTBOUND_SNI = "com.ibm.mq.cfg.SSL.outboundSNI"; + private static final String PROPERTY_CHANNEL_SHARING = "com.ibm.mq.jms.channel.sharing"; + + public MQConfigurationProperties() { + logger.trace("constructor"); + return; + } /** * MQ Queue Manager name @@ -93,49 +104,54 @@ public class MQConfigurationProperties { /** * Override the authentication mode. This - * should not normally be needed with current maintenance levels of MQ V8 or V9, but some earlier levels - * sometimes got get it wrong and then this flag can be set to "false". There is also some confusion about - * whether the field is called "userAuth.." or "useAuth..." So we allow both spellings and use the explicit setting - * to set a different attribute. - * - * @see the KnowledgeCenter + * should not normally be needed with current maintenance levels of MQ V8 or V9, + * but some earlier levels + * sometimes got get it wrong and then this flag can be set to "false". There is + * also some confusion about + * whether the field is called "userAuth.." or "useAuth..." So we allow both + * spellings and use the explicit setting + * to set a different attribute. */ - private boolean userAuthenticationMQCSP; + @SuppressWarnings("unused") + private boolean userAuthenticationMQCSP; // These variables appear unused but are needed to ensure the Spring setters + // recognise them + @SuppressWarnings("unused") private boolean useAuthenticationMQCSP; private boolean authCSP = true; /** - * For TLS connections, you can set either the sslCipherSuite or sslCipherSpec property. + * For TLS connections, you can set either the sslCipherSuite or sslCipherSpec + * property. * For example, "SSL_ECDHE_RSA_WITH_AES_256_GCM_SHA384" - * - * @see the KnowledgeCenter */ private String sslCipherSuite; /** - * For TLS connections, you can set either the sslCipherSuite or sslCipherSpec property. + * For TLS connections, you can set either the sslCipherSuite or sslCipherSpec + * property. * For example, "ECDHE_RSA_AES_256_GCM_SHA384" - * - * @see the KnowledgeCenter */ private String sslCipherSpec; /** - * Type a distinguished name skeleton that must match that provided by the queue manager. - * - * @see the KnowledgeCenter + * Type a distinguished name skeleton that must match that provided by the queue + * manager. */ private String sslPeerName; /** - * Set to true for the IBM JRE CipherSuite name maps; set to false to use the Oracle JRE CipherSuite mapping + * Set to true for the IBM JRE CipherSuite name maps; set to false to use the + * Oracle JRE CipherSuite mapping */ private boolean useIBMCipherMappings = true; /** - * Set to HOSTNAME for connection to OpenShift queue managers where SNI is important. CHANNEL - * can be used for environments where you want to have different certificates associated with different - * channels. The property does not get set unless explicitly configured as an external property so we would + * Set to HOSTNAME for connection to OpenShift queue managers where SNI is + * important. CHANNEL + * can be used for environments where you want to have different certificates + * associated with different + * channels. The property does not get set unless explicitly configured as an + * external property so we would * otherwise use whatever the default behaviour is in the JMS client. */ private String outboundSNI = ""; // HOSTNAME or CHANNEL are the valid alternatives @@ -146,18 +162,24 @@ public class MQConfigurationProperties { private String channelSharing = ""; /** - * Whether to automatically reconnect to the qmgr when in client mode. Values can be YES/NO/QMGR/DISABLED. + * Whether to automatically reconnect to the qmgr when in client mode. Values + * can be YES/NO/QMGR/DISABLED. */ private String reconnect = ""; - private String defaultReconnect = null; // This was the original name but I don't like it. So keep the variable but don't use it. + + // The following variable appears unused but it's needed for Spring + // compatibility on setters + @SuppressWarnings("unused") + private String defaultReconnect = null; // This was the original name but I don't like it. /** - * Enter the uniform resource locator (URL) that identifies the name and location of the file that contains - * the client channel definition table and specifies how the file can be accessed. - * You must set a value for either the Channel property or for the Client Channel Definition Table URL property but not both. + * Enter the uniform resource locator (URL) that identifies the name and + * location of the file that contains + * the client channel definition table and specifies how the file can be + * accessed. + * You must set a value for either the Channel property or for the Client + * Channel Definition Table URL property but not both. * For example, "file:///home/admdata/ccdt1.tab" - * - * @see the KnowledgeCenter */ private String ccdtUrl; @@ -188,32 +210,59 @@ public class MQConfigurationProperties { /** * The key to the SSL Bundle attributes available from Spring Boot 3.1 + * Would perhaps be better called sslBundleName */ private String sslBundle; + /** + * Uniform cluster application balancing options + * Type can be "SIMPLE" or "REQREP" (or its alias of "REQUESTREPLY"). + * The "RA_MANAGED" value for JEE environments does not apply here so is not + * recognised + */ + private String balancingApplicationType = ""; + + /** + * Uniform cluster application balancing options + * Timeout can be set to "DEFAULT", "IMMEDIATE", "NEVER" or a integer number of + * seconds + */ + private String balancingTimeout = ""; + + /** + * Uniform cluster application balancing options, comma-separated. + * Currently recognised options: only IGNORE_TRANS for now, but can + * also supply an integer value directly. + */ + private String balancingOptions = ""; + /** * Additional CF properties that are not explicitly known can be provided - * with the format "ibm.mq.additionalProperties.SOME_PROPERTY=SOME_VALUE". Strings, + * with the format "ibm.mq.additionalProperties.SOME_PROPERTY=SOME_VALUE". + * Strings, * integers and true/false values are recognised. * - * The property is either the actual string for the MQ property, and will usually begin with "XMSC" - * Or it can be the name of the variable in the WMQConstants class. So for example, + * The property is either the actual string for the MQ property, and will + * usually begin with "XMSC" + * Or it can be the name of the variable in the WMQConstants class. So for + * example, * setting the name of a security exit would usually be done in code with - * setStringProperty(WMQConstants.WMQ_SECURITY_EXIT). The value of that constant is + * setStringProperty(WMQConstants.WMQ_SECURITY_EXIT). The value of that constant + * is * "XMSC_WMQ_SECURITY_EXIT" so the external property to set can be either - * "ibm.mq.additionalProperties.XMSC_WMQ_SECURITY_EXIT=com.example.SecExit" + * "ibm.mq.additionalProperties.XMSC_WMQ_SECURITY_EXIT=com.example.SecExit" * or - * "ibm.mq.additionalProperties.WMQ_SECURITY_EXIT=com.example.SecExit" + * "ibm.mq.additionalProperties.WMQ_SECURITY_EXIT=com.example.SecExit" * */ - private Map additionalProperties = new HashMap(); + private Map additionalProperties = new HashMap(); @NestedConfigurationProperty private JmsPoolConnectionFactoryProperties pool = new JmsPoolConnectionFactoryProperties(); @NestedConfigurationProperty private MQConfigurationPropertiesJndi jndi = new MQConfigurationPropertiesJndi(); - + @NestedConfigurationProperty private MQConfigurationPropertiesJks jks = new MQConfigurationPropertiesJks(); @@ -312,9 +361,9 @@ public String getChannelSharing() { } public void setChannelSharing(String channelSharing) { - System.setProperty(PROPERTY_CHANNEL_SHARING,channelSharing); + System.setProperty(PROPERTY_CHANNEL_SHARING, channelSharing); this.channelSharing = channelSharing; - } + } // Both forms of this seem to have been used at different times. So we allow // either to be set. The local field named after the config option is actually @@ -326,12 +375,11 @@ public boolean isUseAuthenticationMQCSP() { public void setUserAuthenticationMQCSP(boolean userAuthenticationMQCSP) { authCSP = userAuthenticationMQCSP; } - + public void setUseAuthenticationMQCSP(boolean useAuthenticationMQCSP) { authCSP = useAuthenticationMQCSP; } - - + public String getSslPeerName() { return sslPeerName; } @@ -355,7 +403,7 @@ public JmsPoolConnectionFactoryProperties getPool() { public MQConfigurationPropertiesJndi getJndi() { return jndi; } - + public MQConfigurationPropertiesJks getJks() { return jks; } @@ -399,7 +447,7 @@ public int getSslKeyResetCount() { public void setSslKeyResetCount(int sslKeyResetCount) { this.sslKeyResetCount = sslKeyResetCount; } - + public String getSslBundle() { return sslBundle; } @@ -432,13 +480,130 @@ public int getReconnectValue() { public String getReconnect() { return reconnect; } + public void setDefaultReconnect(String defaultReconnect) { this.reconnect = defaultReconnect; } + public void setReconnect(String reconnect) { this.reconnect = reconnect; } + public void setBalancingTimeout(String balancingTimeout) { + this.balancingTimeout = balancingTimeout; + } + + public String getBalancingTimeout() { + return balancingTimeout; + } + + public void setBalancingApplicationType(String balancingApplicationType) { + this.balancingApplicationType = balancingApplicationType; + } + + public String getBalancingApplicationType() { + return balancingApplicationType; + } + + public void setBalancingOptions(String balancingOptions) { + this.balancingOptions = balancingOptions; + } + + public String getBalancingOptions() { + return balancingOptions; + } + + public int getBalancingApplicationTypeValue() { + int rc = 0; + if (balancingApplicationType == null || balancingApplicationType.equals("")) + return rc; + + String ba = balancingApplicationType.trim().toUpperCase().replaceAll("_", ""); + switch (ba) { + case "REQREP": + case "REQUESTREPLY": + rc = WMQConstants.WMQ_BALANCING_APPLICATION_TYPE_REQUEST_REPLY; + break; + case "SIMPLE": + rc = WMQConstants.WMQ_BALANCING_APPLICATION_TYPE_SIMPLE; + break; + default: + throw new IllegalArgumentException( + String.format("ApplicationType value \'%s\' not recognised", balancingApplicationType)); + } + return rc; + } + + public int getBalancingTimeoutValue() { + int rc = WMQConstants.WMQ_BALANCING_TIMEOUT_AS_DEFAULT; + + if (balancingTimeout == null || balancingTimeout.equals("")) + return rc; + + String ba = balancingTimeout.toUpperCase(); + switch (ba) { + case "DEFAULT": + rc = WMQConstants.WMQ_BALANCING_TIMEOUT_AS_DEFAULT; + break; + case "IMMEDIATE": + rc = WMQConstants.WMQ_BALANCING_TIMEOUT_IMMEDIATE; + break; + case "NEVER": + rc = WMQConstants.WMQ_BALANCING_TIMEOUT_NEVER; + break; + default: + // Try to parse using a Java-standard Duration string (eg "PT10S"). If that + // fails, try to parse it as an integer but allow the value to have a trailing + // "s". An exception will be thrown if + // it still can't be parsed. Spring Boot does have a more general parser for + // Durations but this should be good enough. + try { + rc = (int) (Duration.parse(ba).toMillis() / 1000); + } + catch (DateTimeParseException e) { + if (ba.endsWith("S")) { + ba = ba.substring(0, ba.length() - 1); + } + rc = Integer.parseInt(ba); + } + break; + } + return rc; + } + + public int getBalancingOptionsValue() { + int rc = WMQConstants.WMQ_BALANCING_OPTIONS_NONE; + if (balancingOptions == null || balancingOptions.isEmpty()) { + return rc; + } + + // This has been written to make it easy if further options are made available. + // Split the option string at commas, then transform each field into a canonical + // version so that simple errors can be avoided. Allow numbers and strings. For + // strings, remove any "_" characters and make the whole thing upper-case. + String boArray[] = balancingOptions.split(","); + for (String bo : boArray) { + String boCanon = bo.trim().toUpperCase().replaceAll("_", ""); + try { + int val = Integer.decode(boCanon); + rc |= val; + } + catch (NumberFormatException e) { + switch (boCanon) { + case "NONE": + rc |= WMQConstants.WMQ_BALANCING_OPTIONS_NONE; + break; + case "IGNORETRANS": + rc |= WMQConstants.WMQ_BALANCING_OPTIONS_IGNORE_TRANSACTIONS; + break; + default: + throw new IllegalArgumentException(String.format("Balancing Options value \'%s\' not recognised", bo)); + } + } + } + return rc; + } + public Map getAdditionalProperties() { return additionalProperties; } @@ -457,18 +622,21 @@ public void traceProperties() { logger.trace("channel : {}", getChannel()); logger.trace("clientId : {}", getClientId()); logger.trace("connName : {}", getConnName()); - logger.trace("reconnectOption : \'{}\' [{}]", getReconnect(),String.format("0x%08X",getReconnectValue())); + logger.trace("reconnectOption : \'{}\' [{}]", getReconnect(), String.format("0x%08X", getReconnectValue())); logger.trace("sslCipherSpec : {}", getSslCipherSpec()); logger.trace("sslCipherSuite : {}", getSslCipherSuite()); logger.trace("sslKeyresetcount: {}", getSslKeyResetCount()); logger.trace("sslPeerName : {}", getSslPeerName()); - logger.trace("sslBundle : {}",getSslBundle()); + logger.trace("sslBundle : {}", getSslBundle()); logger.trace("tempModel : {}", getTempModel()); logger.trace("tempQPrefix : {}", getTempQPrefix()); logger.trace("tempTopicPrefix : {}", getTempTopicPrefix()); logger.trace("user : \'{}\'", getUser()); - /* Obviously we don't want to trace a password. But it is OK to indicate whether one has been configured */ + /* + * Obviously we don't want to trace a password. But it is OK to indicate whether + * one has been configured + */ logger.trace("password set : {}", (getPassword() != null && getPassword().length() > 0) ? "YES" : "NO"); logger.trace("sslFIPSRequired : {}", isSslFIPSRequired()); logger.trace("useIBMCipherMappings : {}", isUseIBMCipherMappings()); @@ -476,31 +644,37 @@ public void traceProperties() { logger.trace("outboundSNI : \'{}\'", getOutboundSNI()); logger.trace("channelSharing : \'{}\'", getChannelSharing()); + logger.trace("balancingAppType : \'{}\' [{}]", getBalancingApplicationType(), getBalancingApplicationTypeValue()); + logger.trace("balancingTimeout : \'{}\' [{}]", getBalancingTimeout(), getBalancingTimeoutValue()); + logger.trace("balancingOptions : \'{}\' [{}]", getBalancingOptions(), getBalancingOptionsValue()); + logger.trace("jndiCF : {}", getJndi().getProviderContextFactory()); logger.trace("jndiProviderUrl : {}", getJndi().getProviderUrl()); - + String pw = getJks().getKeyStorePassword(); - logger.trace("JKS keystore : {}",getJks().getKeyStore()); + logger.trace("JKS keystore : {}", getJks().getKeyStore()); logger.trace("JKS keystore pw set : {}", (pw != null && pw.length() > 0) ? "YES" : "NO"); pw = getJks().getTrustStorePassword(); - logger.trace("JKS truststore : {}",getJks().getTrustStore()); + logger.trace("JKS truststore : {}", getJks().getTrustStore()); logger.trace("JKS truststore pw set : {}", (pw != null && pw.length() > 0) ? "YES" : "NO"); if (additionalProperties.size() > 0) { - for (String s: additionalProperties.keySet()) { - logger.trace("Additional Property - {} : {}",s,additionalProperties.get(s)); + for (String s : additionalProperties.keySet()) { + logger.trace("Additional Property - {} : {}", s, additionalProperties.get(s)); } - } else { + } + else { logger.trace("No additional properties defined"); } if (pool.isEnabled()) { - logger.trace("Pool blockIfFullTimeout : {}",pool.getBlockIfFullTimeout().toString()); - logger.trace("Pool idleTimeout : {}",pool.getIdleTimeout().toString()); - logger.trace("Pool maxConnections : {}",pool.getMaxConnections()); - logger.trace("Pool maxSessionsPerConn : {}",pool.getMaxSessionsPerConnection()); - logger.trace("Pool timeBetweenExpirationCheck : {}",pool.getTimeBetweenExpirationCheck().toString()); - } else { + logger.trace("Pool blockIfFullTimeout : {}", pool.getBlockIfFullTimeout().toString()); + logger.trace("Pool idleTimeout : {}", pool.getIdleTimeout().toString()); + logger.trace("Pool maxConnections : {}", pool.getMaxConnections()); + logger.trace("Pool maxSessionsPerConn : {}", pool.getMaxSessionsPerConnection()); + logger.trace("Pool timeBetweenExpirationCheck : {}", pool.getTimeBetweenExpirationCheck().toString()); + } + else { logger.trace("Pooling is disabled"); } } diff --git a/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQConfigurationSslBundles.java b/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQConfigurationSslBundles.java index 3404f40..98ab4b5 100644 --- a/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQConfigurationSslBundles.java +++ b/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQConfigurationSslBundles.java @@ -20,53 +20,66 @@ package com.ibm.mq.spring.boot; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLSocketFactory; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.ssl.NoSuchSslBundleException; import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.ssl.SslBundles; -import org.springframework.stereotype.Component; +import org.springframework.context.annotation.Configuration; -@Component +@Configuration +@AutoConfigureBefore(MQConfigurationProperties.class) public class MQConfigurationSslBundles { private static Logger logger = LoggerFactory.getLogger(MQConfigurationSslBundles.class); - static SslBundles bundles = null; + /** + * The configured set of SSL Bundles. A map that can be + * references by the sslBundle key. + */ + private static SslBundles sslBundles = null; /* This is called during the initialisation phase */ - public MQConfigurationSslBundles(SslBundles sslBundles) { - logger.trace("constructor - Bundles are {}", (sslBundles == null) ? "null" : "not null"); - bundles = sslBundles; + public MQConfigurationSslBundles(SslBundles _sslBundles) { + logger.trace("constructor - Bundles are {}", (_sslBundles == null) ? "null" : "not null"); + sslBundles = _sslBundles; } - + static boolean isSupported() { logger.trace("SSLBundles are supported"); return true; } /* If the bundle name does not exist, then getBundle throws an exception. Since - there is always some default bundle, we can't rely on there being no bundle. - So we log an error, but otherwise try to continue. + * there is always some default bundle in Boot 3, we can't rely on there being + * a null bundle. So we log an error for your configuration, but otherwise try to continue. */ public static SSLSocketFactory getSSLSocketFactory(String b) { SSLSocketFactory sf = null; - logger.trace("getSSLSocketFactory for {}", b); if (b == null || b.isEmpty()) { + /* Should never get here as the caller has already checked */ + logger.trace("getSSLSocketFactory - null/empty bundle name requested"); return sf; } - if (bundles != null) { + if (sslBundles != null) { try { - SslBundle sb = bundles.getBundle(b); - sf = sb.createSslContext().getSocketFactory(); + SslBundle sb = sslBundles.getBundle(b); + logger.trace("SSL Bundle for {} - found", b); + SSLContext sc = sb.createSslContext(); + // logger.trace("SSL Protocol is {}",sc.getProtocol()); + sf = sc.getSocketFactory(); } catch (NoSuchSslBundleException e) { - logger.error("No SSL bundle found for {}", b); + logger.error("SSL bundle for {} - not found", b); } } return sf; } + } \ No newline at end of file diff --git a/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQConnectionFactoryFactory.java b/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQConnectionFactoryFactory.java index ce2c7f7..42dccf9 100644 --- a/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQConnectionFactoryFactory.java +++ b/mq-jms-spring-boot-starter/src/main/java/com/ibm/mq/spring/boot/MQConnectionFactoryFactory.java @@ -1,5 +1,5 @@ /* - * Copyright © 2018,2022 IBM Corp. All rights reserved. + * Copyright © 2018,2023 IBM Corp. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at @@ -67,7 +67,7 @@ public T createConnectionFactory(Class factor /* From Spring Boot 3.1, we can put sets of SSL configuration items in a bundle */ /* The bundle name takes priority over the ibm.mq.jks properties */ if (MQConfigurationSslBundles.isSupported() && isNotNullOrEmpty(sslBundle)) { - sf = MQConfigurationSslBundles.getSSLSocketFactory(this.properties.getSslBundle()); + sf = MQConfigurationSslBundles.getSSLSocketFactory(sslBundle); } else { configureTLSStores(this.properties); } @@ -124,7 +124,6 @@ public T createConnectionFactory(Class factor * This method allows someone to create their own CF and then have it configured * using the same MQConfigurationProperties class - which might have been assigned * from a different prefix in the properties file. - * */ public static void configureConnectionFactory(MQConnectionFactory cf, MQConfigurationProperties props) throws JMSException { // Should usually provide a queue manager name but it can be empty, to connect to the @@ -136,8 +135,10 @@ public static void configureConnectionFactory(MQConnectionFactory cf, MQConfigur props.traceProperties(); String qmName = props.getQueueManager(); - cf.setStringProperty(WMQConstants.WMQ_QUEUE_MANAGER, qmName); - + if (!isNullOrEmpty(qmName)) { + cf.setStringProperty(WMQConstants.WMQ_QUEUE_MANAGER, qmName); + } + // Use the channel name to decide whether to try to connect locally or as a client. If the queue manager // code has been installed locally, then this connection will try to use native JNI bindings to match. String channel = props.getChannel(); @@ -169,6 +170,19 @@ public static void configureConnectionFactory(MQConnectionFactory cf, MQConfigur if (!isNullOrEmpty(props.getReconnect())) { cf.setIntProperty(WMQConstants.WMQ_CLIENT_RECONNECT_OPTIONS, props.getReconnectValue()); } + + /* Balancing options for Uniform clusters came available from 9.3.4 */ + if (!isNullOrEmpty(props.getBalancingApplicationType())) { + cf.setIntProperty(WMQConstants.WMQ_BALANCING_APPLICATION_TYPE,props.getBalancingApplicationTypeValue()); + } + + if (!isNullOrEmpty(props.getBalancingOptions())) { + cf.setIntProperty(WMQConstants.WMQ_BALANCING_OPTIONS,props.getBalancingOptionsValue()); + } + + if (!isNullOrEmpty(props.getBalancingTimeout())) { + cf.setIntProperty(WMQConstants.WMQ_BALANCING_TIMEOUT,props.getBalancingTimeoutValue()); + } } String applicationName = props.getApplicationName(); if (!isNullOrEmpty(applicationName)) { @@ -178,13 +192,16 @@ public static void configureConnectionFactory(MQConnectionFactory cf, MQConfigur // Setup the authentication. If there is a userid defined, prefer to use the CSP model for // password checking. That is more general than the cf.connect(user,pass) method which has // some restrictions in the MQ client. But it is possible to override the choice via a - // property, for some compatibility requirements. + // property, for some compatibility requirements. It is possible to have a blank userid + // while also setting a password - the queue manager authentication mechanism would then + // be responsible for allocating an identity. String u = props.getUser(); if (!isNullOrEmpty(u)) { cf.setStringProperty(WMQConstants.USERID, u); - String p = props.getPassword(); - if (!isNullOrEmpty(p)) - cf.setStringProperty(WMQConstants.PASSWORD, p); + } + String p = props.getPassword(); + if (!isNullOrEmpty(p)) { + cf.setStringProperty(WMQConstants.PASSWORD, p); cf.setBooleanProperty(WMQConstants.USER_AUTHENTICATION_MQCSP, props.isUseAuthenticationMQCSP()); } @@ -289,7 +306,8 @@ public static void configureConnectionFactory(MQConnectionFactory cf, MQConfigur * Access to Java keystores can be controlled by system properties. These are usually * given with -D options on the command line but we can set them here instead. The Spring properties * that drive these will be "ibm.mq.jks.keyStore=" ... For historic reasons, we set the com.ibm.ssl versions - * as well as the regular javax.net.ssl properties. + * as well as the regular javax.net.ssl properties. Deprecated in Spring Boot 3 where we try to use + * SSLBundles instead. */ private static void configureTLSStores(MQConfigurationProperties props) { String prefixes[] = {"javax.net.ssl.","com.ibm.ssl."}; diff --git a/mq-jms-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/mq-jms-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports index 9f8d04c..b865dc4 100644 --- a/mq-jms-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports +++ b/mq-jms-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -1,3 +1,3 @@ # Used in Spring Boot 3 com.ibm.mq.spring.boot.MQAutoConfiguration -com.ibm.mq.spring.boot.MQConfigurationSslBundles +#com.ibm.mq.spring.boot.MQConfigurationSslBundles diff --git a/samples/s1/build.gradle b/samples/s1/build.gradle index 661e911..eccc082 100644 --- a/samples/s1/build.gradle +++ b/samples/s1/build.gradle @@ -4,17 +4,14 @@ buildscript { mavenCentral() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE") - } + classpath("org.springframework.boot:spring-boot-gradle-plugin:2.7.17") } } - apply plugin: 'java' apply plugin: 'org.springframework.boot' // The designated version should match the current version in the root of the repo -ext.starterVersion = '2.7.14' - +ext.starterVersion = '2.7.17' // The local, flatDir configuration lets us use a modified version from // this repository without needing it released via maven repositories { @@ -26,5 +23,5 @@ repositories { } dependencies { - implementation(group: "com.ibm.mq", name:"mq-jms-spring-boot-starter",version: starterVersion) + implementation(group:"com.ibm.mq", name:"mq-jms-spring-boot-starter",version: starterVersion) } diff --git a/samples/s2.tls.jms3/build.gradle b/samples/s2.tls.jms3/build.gradle index d8d2561..4500ead 100644 --- a/samples/s2.tls.jms3/build.gradle +++ b/samples/s2.tls.jms3/build.gradle @@ -4,17 +4,14 @@ buildscript { mavenCentral() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE") - } + classpath("org.springframework.boot:spring-boot-gradle-plugin:3.1.5") } } - apply plugin: 'java' apply plugin: 'org.springframework.boot' // The designated version should match the current version in the root of the repo -ext.starterVersion = '3.1.2' - +ext.starterVersion = '3.1.5' // The local, flatDir configuration lets us use a modified version from // this repository without needing it released via maven repositories { @@ -29,6 +26,7 @@ dependencies { implementation(group:"com.ibm.mq", name:"mq-jms-spring-boot-starter", version:starterVersion) } +// Do not need these properties as they are now supplied via SslBundle attributes bootRun { //systemProperty('javax.net.ssl.trustStore','key.jks') //systemProperty('javax.net.ssl.trustStorePassword','passw0rd') diff --git a/samples/s2.tls.jms3/src/main/java/sample2/Application.java b/samples/s2.tls.jms3/src/main/java/sample2/Application.java index e6c8039..755a208 100644 --- a/samples/s2.tls.jms3/src/main/java/sample2/Application.java +++ b/samples/s2.tls.jms3/src/main/java/sample2/Application.java @@ -1,5 +1,5 @@ /* - * Copyright © 2017, 2020 IBM Corp. All rights reserved. + * Copyright © 2017, 2023 IBM Corp. All rights reserved. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at @@ -30,8 +30,6 @@ import java.util.Date; -import javax.net.ssl.SSLSocketFactory; - import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ConfigurableApplicationContext; @@ -44,9 +42,7 @@ import com.ibm.mq.jakarta.jms.MQConnectionFactory; import com.ibm.mq.spring.boot.MQConnectionFactoryCustomizer; -import com.ibm.msg.client.jakarta.wmq.WMQConstants; -import jakarta.jms.JMSException; import jakarta.jms.Message; @SpringBootApplication diff --git a/samples/s2.tls.jms3/src/main/resources/application.yml b/samples/s2.tls.jms3/src/main/resources/application.yml index bdfa904..94259cc 100644 --- a/samples/s2.tls.jms3/src/main/resources/application.yml +++ b/samples/s2.tls.jms3/src/main/resources/application.yml @@ -5,14 +5,18 @@ ibm: connName: "localhost(1414)" sslCipherSpec: "ANY_TLS12_OR_HIGHER" sslBundle: "ibmmq" - + # Setup a "bundle" pointing at the truststore. Available from Spring Boot 3.1. # The key to the bundle is given in the ibm.mq.sslBundle attribute. spring: + jms: + cache: + enabled: true ssl: bundle: jks: ibmmq: + # protocol: TLSv1.2 truststore: location: "key.jks" password: "passw0rd" @@ -23,8 +27,10 @@ spring: type: "JKS" logging: + pattern: + console: "%logger{36} - %msg%n" level: - # root: "TRACE" + root: INFO com: ibm: mq: diff --git a/samples/s2.tls/build.gradle b/samples/s2.tls/build.gradle index 9605ad8..3223484 100644 --- a/samples/s2.tls/build.gradle +++ b/samples/s2.tls/build.gradle @@ -4,8 +4,7 @@ buildscript { mavenCentral() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE") - } + classpath("org.springframework.boot:spring-boot-gradle-plugin:2.7.17") } } @@ -13,8 +12,7 @@ apply plugin: 'java' apply plugin: 'org.springframework.boot' // The designated version should match the current version in the root of the repo -ext.starterVersion = '2.7.14' - +ext.starterVersion = '2.7.17' // The local, flatDir configuration lets us use a modified version from // this repository without needing it released via maven repositories { diff --git a/samples/s2/build.gradle b/samples/s2/build.gradle index fd1a413..5b8da74 100644 --- a/samples/s2/build.gradle +++ b/samples/s2/build.gradle @@ -4,17 +4,14 @@ buildscript { mavenCentral() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE") - } + classpath("org.springframework.boot:spring-boot-gradle-plugin:2.7.17") } } - apply plugin: 'java' -apply plugin: 'org.springframework.boot' +apply plugin: 'org.springframework.boot' // The designated version should match the current version in the root of the repo -ext.starterVersion = '2.7.14' - +ext.starterVersion = '2.7.17' // The local, flatDir configuration lets us use a modified version from // this repository without needing it released via maven repositories { diff --git a/samples/s3.jms3/build.gradle b/samples/s3.jms3/build.gradle index 8977d03..c7035ec 100644 --- a/samples/s3.jms3/build.gradle +++ b/samples/s3.jms3/build.gradle @@ -4,8 +4,7 @@ buildscript { mavenCentral() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE") - } + classpath("org.springframework.boot:spring-boot-gradle-plugin:3.1.5") } } @@ -13,8 +12,7 @@ apply plugin: 'java' apply plugin: 'org.springframework.boot' // The designated version should match the current version in the root of the repo -ext.starterVersion = '3.1.2' - +ext.starterVersion = '3.1.5' // The local, flatDir configuration lets us use a modified version from // this repository without needing it released via maven diff --git a/samples/s3/build.gradle b/samples/s3/build.gradle index fd1a413..e5970f1 100644 --- a/samples/s3/build.gradle +++ b/samples/s3/build.gradle @@ -4,17 +4,14 @@ buildscript { mavenCentral() } dependencies { - classpath("org.springframework.boot:spring-boot-gradle-plugin:2.0.5.RELEASE") - } + classpath("org.springframework.boot:spring-boot-gradle-plugin:2.7.17") } } - apply plugin: 'java' apply plugin: 'org.springframework.boot' // The designated version should match the current version in the root of the repo -ext.starterVersion = '2.7.14' - +ext.starterVersion = '2.7.17' // The local, flatDir configuration lets us use a modified version from // this repository without needing it released via maven repositories {