From 3fb6afb80c0c530e6017dd6796028f11e4c36221 Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Sat, 10 Oct 2026 12:01:43 -0500 Subject: [PATCH 01/14] refactor: convert build-logic docs-core and grails-gradle common/model to Groovy Converts the remaining Java sources of build-logic/docs-core, grails-gradle/common and grails-gradle/model to Groovy. Part of the per-project split requested by the maintainers on #16351; end state taken from the 9.0.x-java-to-groovy branch (origin/9.0.x..e55fd9e0eb). Co-Authored-By: Claude Sonnet 5.5 --- ...egory.java => StringEscapeCategory.groovy} | 25 +- .../internal/StringEscapeCategorySpec.groovy | 41 ++ ...erFeature.java => XmlParserFeature.groovy} | 26 +- ...ctionUtils.java => CollectionUtils.groovy} | 50 +- ...sNameUtils.java => GrailsNameUtils.groovy} | 396 +++++------ .../util/{Holder.java => Holder.groovy} | 59 +- ...{SupplierUtil.java => SupplierUtil.groovy} | 59 +- .../{CommandLine.java => CommandLine.groovy} | 65 +- ...neParser.java => CommandLineParser.groovy} | 243 ++++--- .../build/parsing/DefaultCommandLine.groovy | 230 ++++++ .../build/parsing/DefaultCommandLine.java | 232 ------ .../parsing/{Option.java => Option.groovy} | 28 +- ...seException.java => ParseException.groovy} | 18 +- ...a => AbstractFileResolvingResource.groovy} | 106 ++- ...Resource.java => ClassPathResource.groovy} | 161 +++-- ...ader.java => DefaultResourceLoader.groovy} | 67 +- ...esource.java => FileSystemResource.groovy} | 109 ++- ...eUtils.java => GrailsResourceUtils.groovy} | 667 +++++++++--------- .../{Resource.java => Resource.groovy} | 33 +- ...ourceLoader.java => ResourceLoader.groovy} | 11 +- ...pringIOUtils.java => SpringIOUtils.groovy} | 356 +++++----- .../{UrlResource.java => UrlResource.groovy} | 109 ++- .../grails/util/CollectionUtilsSpec.groovy | 88 +++ .../test/groovy/grails/util/HolderSpec.groovy | 65 ++ .../grails/util/SupplierUtilSpec.groovy | 103 +++ .../grails/build/parsing/OptionSpec.groovy | 58 ++ .../io/support/ClassPathResourceSpec.groovy | 105 +++ .../support/DefaultResourceLoaderSpec.groovy | 85 +++ .../io/support/FileSystemResourceSpec.groovy | 116 +++ .../io/support/GrailsResourceUtilsSpec.groovy | 136 ++++ .../io/support/SpringIOUtilsSpec.groovy | 120 ++++ .../grails/io/support/UrlResourceSpec.groovy | 109 +++ 32 files changed, 2531 insertions(+), 1545 deletions(-) rename build-logic/docs-core/src/main/groovy/grails/doc/internal/{StringEscapeCategory.java => StringEscapeCategory.groovy} (67%) create mode 100644 build-logic/docs-core/src/test/groovy/grails/doc/internal/StringEscapeCategorySpec.groovy rename grails-gradle/common/src/main/groovy/org/apache/grails/gradle/common/{XmlParserFeature.java => XmlParserFeature.groovy} (82%) rename grails-gradle/model/src/main/groovy/grails/util/{CollectionUtils.java => CollectionUtils.groovy} (59%) rename grails-gradle/model/src/main/groovy/grails/util/{GrailsNameUtils.java => GrailsNameUtils.groovy} (67%) rename grails-gradle/model/src/main/groovy/grails/util/{Holder.java => Holder.groovy} (65%) rename grails-gradle/model/src/main/groovy/grails/util/{SupplierUtil.java => SupplierUtil.groovy} (64%) rename grails-gradle/model/src/main/groovy/org/grails/build/parsing/{CommandLine.java => CommandLine.groovy} (62%) rename grails-gradle/model/src/main/groovy/org/grails/build/parsing/{CommandLineParser.java => CommandLineParser.groovy} (52%) create mode 100644 grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy delete mode 100644 grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.java rename grails-gradle/model/src/main/groovy/org/grails/build/parsing/{Option.java => Option.groovy} (67%) rename grails-gradle/model/src/main/groovy/org/grails/build/parsing/{ParseException.java => ParseException.groovy} (75%) rename grails-gradle/model/src/main/groovy/org/grails/io/support/{AbstractFileResolvingResource.java => AbstractFileResolvingResource.groovy} (66%) rename grails-gradle/model/src/main/groovy/org/grails/io/support/{ClassPathResource.java => ClassPathResource.groovy} (69%) rename grails-gradle/model/src/main/groovy/org/grails/io/support/{DefaultResourceLoader.java => DefaultResourceLoader.groovy} (72%) rename grails-gradle/model/src/main/groovy/org/grails/io/support/{FileSystemResource.java => FileSystemResource.groovy} (62%) rename grails-gradle/model/src/main/groovy/org/grails/io/support/{GrailsResourceUtils.java => GrailsResourceUtils.groovy} (60%) rename grails-gradle/model/src/main/groovy/org/grails/io/support/{Resource.java => Resource.groovy} (86%) rename grails-gradle/model/src/main/groovy/org/grails/io/support/{ResourceLoader.java => ResourceLoader.groovy} (92%) rename grails-gradle/model/src/main/groovy/org/grails/io/support/{SpringIOUtils.java => SpringIOUtils.groovy} (61%) rename grails-gradle/model/src/main/groovy/org/grails/io/support/{UrlResource.java => UrlResource.groovy} (66%) create mode 100644 grails-gradle/model/src/test/groovy/grails/util/CollectionUtilsSpec.groovy create mode 100644 grails-gradle/model/src/test/groovy/grails/util/HolderSpec.groovy create mode 100644 grails-gradle/model/src/test/groovy/grails/util/SupplierUtilSpec.groovy create mode 100644 grails-gradle/model/src/test/groovy/org/grails/build/parsing/OptionSpec.groovy create mode 100644 grails-gradle/model/src/test/groovy/org/grails/io/support/ClassPathResourceSpec.groovy create mode 100644 grails-gradle/model/src/test/groovy/org/grails/io/support/DefaultResourceLoaderSpec.groovy create mode 100644 grails-gradle/model/src/test/groovy/org/grails/io/support/FileSystemResourceSpec.groovy create mode 100644 grails-gradle/model/src/test/groovy/org/grails/io/support/GrailsResourceUtilsSpec.groovy create mode 100644 grails-gradle/model/src/test/groovy/org/grails/io/support/UrlResourceSpec.groovy diff --git a/build-logic/docs-core/src/main/groovy/grails/doc/internal/StringEscapeCategory.java b/build-logic/docs-core/src/main/groovy/grails/doc/internal/StringEscapeCategory.groovy similarity index 67% rename from build-logic/docs-core/src/main/groovy/grails/doc/internal/StringEscapeCategory.java rename to build-logic/docs-core/src/main/groovy/grails/doc/internal/StringEscapeCategory.groovy index a863a760d1e..b8b4d30bc97 100644 --- a/build-logic/docs-core/src/main/groovy/grails/doc/internal/StringEscapeCategory.java +++ b/build-logic/docs-core/src/main/groovy/grails/doc/internal/StringEscapeCategory.groovy @@ -17,38 +17,39 @@ * under the License. */ -package grails.doc.internal; +package grails.doc.internal -import java.net.URI; -import java.net.URISyntaxException; +import groovy.transform.CompileStatic +import org.apache.commons.text.StringEscapeUtils -import org.apache.commons.text.StringEscapeUtils; +@CompileStatic +class StringEscapeCategory { -public class StringEscapeCategory { private StringEscapeCategory() { } public static String encodeAsUrlPath(String str) { try { - String uri = new URI("http", "localhost", '/' + str, "").toASCIIString(); - return uri.substring(17, uri.length() - 1); + String uri = new URI('http', 'localhost', '/' + str, '').toASCIIString() + return uri.substring(17, uri.length() - 1) } catch (URISyntaxException ex) { - throw new RuntimeException(ex); + throw new RuntimeException(ex) } } public static String encodeAsUrlFragment(String str) { try { - String uri = new URI("http", "localhost", "/", str).toASCIIString(); - return uri.substring(18, uri.length()); + String uri = new URI('http', 'localhost', '/', str).toASCIIString() + return uri.substring(18, uri.length()) } catch (URISyntaxException ex) { - throw new RuntimeException(ex); + throw new RuntimeException(ex) } } public static String encodeAsHtml(String str) { - return StringEscapeUtils.escapeHtml4(str); + return StringEscapeUtils.escapeHtml4(str) } + } diff --git a/build-logic/docs-core/src/test/groovy/grails/doc/internal/StringEscapeCategorySpec.groovy b/build-logic/docs-core/src/test/groovy/grails/doc/internal/StringEscapeCategorySpec.groovy new file mode 100644 index 00000000000..6df68c63776 --- /dev/null +++ b/build-logic/docs-core/src/test/groovy/grails/doc/internal/StringEscapeCategorySpec.groovy @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package grails.doc.internal + +import spock.lang.Specification + +class StringEscapeCategorySpec extends Specification { + + void 'empty and reserved characters are encoded consistently'() { + expect: + StringEscapeCategory.encodeAsUrlPath('') == '' + StringEscapeCategory.encodeAsUrlPath('a/b?c#d') == 'a/b%3Fc%23d' + StringEscapeCategory.encodeAsUrlFragment('') == '' + StringEscapeCategory.encodeAsUrlFragment('a/b?c#d') == 'a/b?c%23d' + StringEscapeCategory.encodeAsHtml('') == '' + StringEscapeCategory.encodeAsHtml('"quoted" & \'single\'') == '"quoted" & \'single\'' + StringEscapeCategory.encodeAsHtml(null) == null + } + + void 'the category is a static utility'() { + expect: + StringEscapeCategory.declaredConstructors.every { java.lang.reflect.Modifier.isPrivate(it.modifiers) } + } + +} diff --git a/grails-gradle/common/src/main/groovy/org/apache/grails/gradle/common/XmlParserFeature.java b/grails-gradle/common/src/main/groovy/org/apache/grails/gradle/common/XmlParserFeature.groovy similarity index 82% rename from grails-gradle/common/src/main/groovy/org/apache/grails/gradle/common/XmlParserFeature.java rename to grails-gradle/common/src/main/groovy/org/apache/grails/gradle/common/XmlParserFeature.groovy index fb10d909fb0..ad8377eadf2 100644 --- a/grails-gradle/common/src/main/groovy/org/apache/grails/gradle/common/XmlParserFeature.java +++ b/grails-gradle/common/src/main/groovy/org/apache/grails/gradle/common/XmlParserFeature.groovy @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package org.apache.grails.gradle.common; +package org.apache.grails.gradle.common /** * Registered SAX and Xerces parser feature identifiers used to harden XML parsing. @@ -36,7 +36,7 @@ * * @since 8.0.0 */ -public enum XmlParserFeature { +enum XmlParserFeature { /** * Rejects any document carrying a {@code DOCTYPE} declaration. @@ -47,22 +47,22 @@ public enum XmlParserFeature { * {@code web.xml} — routinely carry a DOCTYPE, so their readers need a parser that leaves this * disabled while keeping the entity and DTD features below switched off. */ - DISALLOW_DOCTYPE_DECL("http://apache.org/xml/features/disallow-doctype-decl"), + DISALLOW_DOCTYPE_DECL('http://apache.org/xml/features/disallow-doctype-decl'), /** * Blocks resolution of external general entities, the primary XXE vector. */ - EXTERNAL_GENERAL_ENTITIES("http://xml.org/sax/features/external-general-entities"), + EXTERNAL_GENERAL_ENTITIES('http://xml.org/sax/features/external-general-entities'), /** * Blocks resolution of external parameter entities. */ - EXTERNAL_PARAMETER_ENTITIES("http://xml.org/sax/features/external-parameter-entities"), + EXTERNAL_PARAMETER_ENTITIES('http://xml.org/sax/features/external-parameter-entities'), /** * Stops the parser building a grammar from a DTD. */ - LOAD_DTD_GRAMMAR("http://apache.org/xml/features/nonvalidating/load-dtd-grammar"), + LOAD_DTD_GRAMMAR('http://apache.org/xml/features/nonvalidating/load-dtd-grammar'), /** * Skips external DTD subsets instead of retrieving them. @@ -71,24 +71,24 @@ public enum XmlParserFeature { * raises an error when a document references an external DTD. Skipping is what allows a * descriptor that names a DTD, such as a JSP 1.2 tag library, to parse without retrieving it. */ - LOAD_EXTERNAL_DTD("http://apache.org/xml/features/nonvalidating/load-external-dtd"); + LOAD_EXTERNAL_DTD('http://apache.org/xml/features/nonvalidating/load-external-dtd') - private final String featureName; + private final String featureName XmlParserFeature(String featureName) { - this.featureName = featureName; + this.featureName = featureName } /** * @return the registered identifier to pass to {@code setFeature} */ - public String getFeatureName() { - return featureName; + String getFeatureName() { + return featureName } @Override - public String toString() { - return featureName; + String toString() { + return featureName } } diff --git a/grails-gradle/model/src/main/groovy/grails/util/CollectionUtils.java b/grails-gradle/model/src/main/groovy/grails/util/CollectionUtils.groovy similarity index 59% rename from grails-gradle/model/src/main/groovy/grails/util/CollectionUtils.java rename to grails-gradle/model/src/main/groovy/grails/util/CollectionUtils.groovy index 7834bc2ec50..583bcf2d7eb 100644 --- a/grails-gradle/model/src/main/groovy/grails/util/CollectionUtils.java +++ b/grails-gradle/model/src/main/groovy/grails/util/CollectionUtils.groovy @@ -16,59 +16,52 @@ * specific language governing permissions and limitations * under the License. */ -package grails.util; +package grails.util -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; +import groovy.transform.CompileStatic /** * Collection utility methods. * * @author Burt Beckwith */ -public class CollectionUtils { +@CompileStatic +class CollectionUtils { private CollectionUtils() { // static only } - @SuppressWarnings({ "rawtypes", "unchecked" }) - public static Map newMap(Object... keysAndValues) { + @SuppressWarnings([ 'rawtypes', 'unchecked' ]) + static Map newMap(Object... keysAndValues) { if (keysAndValues == null) { - return Collections.emptyMap(); + return Collections.emptyMap() } if (keysAndValues.length % 2 == 1) { - throw new IllegalArgumentException("Must have an even number of keys and values"); + throw new IllegalArgumentException('Must have an even number of keys and values') } - Map map = new HashMap<>(); + Map map = new HashMap<>() for (int i = 0; i < keysAndValues.length; i += 2) { - map.put((K) keysAndValues[i], (V) keysAndValues[i + 1]); + map.put((K) keysAndValues[i], (V) keysAndValues[i + 1]) } - return map; + return map } - public static Set newSet(T... values) { + static Set newSet(T... values) { if (values == null) { - return Collections.emptySet(); + return Collections.emptySet() } - return new HashSet<>(Arrays.asList(values)); + return new HashSet<>(Arrays.asList(values)) } - public static List newList(T... values) { + static List newList(T... values) { if (values == null) { - return Collections.emptyList(); + return Collections.emptyList() } - return new ArrayList<>(Arrays.asList(values)); + return new ArrayList<>(Arrays.asList(values)) } /** @@ -78,11 +71,12 @@ public static List newList(T... values) { * @param key The key that holds the child map * @return The child map */ - public static Map getOrCreateChildMap(Map parent, String key) { - Object o = parent.get(key); + static Map getOrCreateChildMap(Map parent, String key) { + Object o = parent.get(key) if (o instanceof Map) { - return (Map) o; + return (Map) o } - return new LinkedHashMap(); + return new LinkedHashMap() } + } diff --git a/grails-gradle/model/src/main/groovy/grails/util/GrailsNameUtils.java b/grails-gradle/model/src/main/groovy/grails/util/GrailsNameUtils.groovy similarity index 67% rename from grails-gradle/model/src/main/groovy/grails/util/GrailsNameUtils.java rename to grails-gradle/model/src/main/groovy/grails/util/GrailsNameUtils.groovy index 2fe96736271..19be1e7f819 100644 --- a/grails-gradle/model/src/main/groovy/grails/util/GrailsNameUtils.java +++ b/grails-gradle/model/src/main/groovy/grails/util/GrailsNameUtils.groovy @@ -16,25 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -package grails.util; +package grails.util -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.List; -import java.util.Locale; -import java.util.Map; -import java.util.Set; +import groovy.transform.CompileStatic /** * Utility methods for converting between different name types, * for example from class names -> property names and vice-versa. The * key aspect of this class is that it has no dependencies outside the JDK! */ -public class GrailsNameUtils { +@CompileStatic +class GrailsNameUtils { - private static final String PROPERTY_SET_PREFIX = "set"; - private static final String PROPERTY_GET_PREFIX = "get"; + private static final String PROPERTY_SET_PREFIX = 'set' + private static final String PROPERTY_GET_PREFIX = 'get' private GrailsNameUtils() { } @@ -44,9 +39,9 @@ private GrailsNameUtils() { * @param propertyName The property name * @return The setter equivalent */ - public static String getSetterName(String propertyName) { - final String suffix = getSuffixForGetterOrSetter(propertyName); - return PROPERTY_SET_PREFIX + suffix; + static String getSetterName(String propertyName) { + final String suffix = getSuffixForGetterOrSetter(propertyName) + return PROPERTY_SET_PREFIX + suffix } /** @@ -54,21 +49,21 @@ public static String getSetterName(String propertyName) { * @param propertyName * @return The name for the getter method for this property, if it were to exist, i.e. getConstraints */ - public static String getGetterName(String propertyName) { - final String suffix = getSuffixForGetterOrSetter(propertyName); - return PROPERTY_GET_PREFIX + suffix; + static String getGetterName(String propertyName) { + final String suffix = getSuffixForGetterOrSetter(propertyName) + return PROPERTY_GET_PREFIX + suffix } private static String getSuffixForGetterOrSetter(String propertyName) { - final String suffix; + final String suffix if (propertyName.length() > 1 && Character.isLowerCase(propertyName.charAt(0)) && Character.isUpperCase(propertyName.charAt(1))) { - suffix = propertyName; + suffix = propertyName } else { - suffix = Character.toUpperCase(propertyName.charAt(0)) + propertyName.substring(1); + suffix = String.valueOf(Character.toUpperCase(propertyName.charAt(0))) + propertyName.substring(1) } - return suffix; + return suffix } /** @@ -78,16 +73,16 @@ private static String getSuffixForGetterOrSetter(String propertyName) { * @param trailingName The trailing name * @return The class name */ - public static String getClassName(String logicalName, String trailingName) { + static String getClassName(String logicalName, String trailingName) { if (isBlank(logicalName)) { - throw new IllegalArgumentException("Argument [logicalName] cannot be null or blank"); + throw new IllegalArgumentException('Argument [logicalName] cannot be null or blank') } - String className = logicalName.substring(0, 1).toUpperCase(Locale.ENGLISH) + logicalName.substring(1); + String className = logicalName.substring(0, 1).toUpperCase(Locale.ENGLISH) + logicalName.substring(1) if (trailingName != null) { - className = className + trailingName; + className = className + trailingName } - return className; + return className } /** @@ -95,10 +90,10 @@ public static String getClassName(String logicalName, String trailingName) { * * @param cls The class name */ - public static String getFullClassName(Class cls) { - String className = cls.getName(); + static String getFullClassName(Class cls) { + String className = cls.getName() - return getFullClassName(className); + return getFullClassName(className) } /** @@ -106,12 +101,12 @@ public static String getFullClassName(Class cls) { * * @param className The class name */ - public static String getFullClassName(String className) { - final int i = className.indexOf('$'); + static String getFullClassName(String className) { + final int i = className.indexOf('$') if (i > -1) { - className = className.substring(0, i); + className = className.substring(0, i) } - return className; + return className } /** @@ -120,8 +115,8 @@ public static String getFullClassName(String className) { * @param logicalName The logical name * @return The class name */ - public static String getClassName(String logicalName) { - return getClassName(logicalName, ""); + static String getClassName(String logicalName) { + return getClassName(logicalName, '') } /** @@ -130,25 +125,25 @@ public static String getClassName(String logicalName) { * @param name The name to convert * @return The property name representation */ - public static String getClassNameRepresentation(String name) { + static String getClassNameRepresentation(String name) { if (name == null || name.length() == 0) { - return ""; + return '' } - StringBuilder buf = new StringBuilder(); - String[] tokens = name.split("[^\\w\\d]"); - for (String token1 : tokens) { - String token = token1.trim(); - int length = token.length(); + StringBuilder buf = new StringBuilder() + String[] tokens = name.split('[^\\w\\d]') + for (String token1 in tokens) { + String token = token1.trim() + int length = token.length() if (length > 0) { - buf.append(token.substring(0, 1).toUpperCase(Locale.ENGLISH)); + buf.append(token.substring(0, 1).toUpperCase(Locale.ENGLISH)) if (length > 1) { - buf.append(token.substring(1)); + buf.append(token.substring(1)) } } } - return buf.toString(); + return buf.toString() } /** @@ -159,20 +154,20 @@ public static String getClassNameRepresentation(String name) { */ private static String getClassNameForLowerCaseHyphenSeparatedName(String name) { // Handle null and empty strings. - if (isBlank(name)) return name; + if (isBlank(name)) return name if (name.indexOf('-') == -1) { - return name.substring(0, 1).toUpperCase() + name.substring(1); + return name.substring(0, 1).toUpperCase() + name.substring(1) } - StringBuilder buf = new StringBuilder(); - String[] tokens = name.split("-"); - for (String token : tokens) { - if (token == null || token.length() == 0) continue; + StringBuilder buf = new StringBuilder() + String[] tokens = name.split('-') + for (String token in tokens) { + if (token == null || token.length() == 0) continue buf.append(token.substring(0, 1).toUpperCase()) - .append(token.substring(1)); + .append(token.substring(1)) } - return buf.toString(); + return buf.toString() } /** @@ -183,8 +178,8 @@ private static String getClassNameForLowerCaseHyphenSeparatedName(String name) { * @param trailingName The trailing name such as "Controller" or "TagLib" * @return The logical class name */ - public static String getLogicalName(Class clazz, String trailingName) { - return getLogicalName(clazz.getName(), trailingName); + static String getLogicalName(Class clazz, String trailingName) { + return getLogicalName(clazz.getName(), trailingName) } /** @@ -193,26 +188,26 @@ public static String getLogicalName(Class clazz, String trailingName) { * @param trailingName The trailing name * @return The logical name */ - public static String getLogicalName(String name, String trailingName) { + static String getLogicalName(String name, String trailingName) { if (isBlank(trailingName)) { - return name; + return name } - String shortName = getShortName(name); + String shortName = getShortName(name) if (shortName.indexOf(trailingName) == -1) { - return name; + return name } - return shortName.substring(0, shortName.length() - trailingName.length()); + return shortName.substring(0, shortName.length() - trailingName.length()) } - public static String getLogicalPropertyName(String className, String trailingName) { + static String getLogicalPropertyName(String className, String trailingName) { if (!isBlank(className) && !isBlank(trailingName)) { if (className.length() == trailingName.length() + 1 && className.endsWith(trailingName)) { - return className.substring(0, 1).toLowerCase(); + return className.substring(0, 1).toLowerCase() } } - return getLogicalName(getPropertyName(className), trailingName); + return getLogicalName(getPropertyName(className), trailingName) } /** @@ -220,8 +215,8 @@ public static String getLogicalPropertyName(String className, String trailingNam * @param name The name to convert * @return The property name version */ - public static String getPropertyName(String name) { - return getPropertyNameRepresentation(name); + static String getPropertyName(String name) { + return getPropertyNameRepresentation(name) } /** @@ -229,8 +224,8 @@ public static String getPropertyName(String name) { * @param clazz The clazz to convert * @return The property name version */ - public static String getPropertyName(Class clazz) { - return getPropertyNameRepresentation(clazz); + static String getPropertyName(Class clazz) { + return getPropertyNameRepresentation(clazz) } /** @@ -239,8 +234,8 @@ public static String getPropertyName(Class clazz) { * @param targetClass The class to get the property name for * @return A property name reperesentation of the class name (eg. MyClass becomes myClass) */ - public static String getPropertyNameRepresentation(Class targetClass) { - return getPropertyNameRepresentation(getShortName(targetClass)); + static String getPropertyNameRepresentation(Class targetClass) { + return getPropertyNameRepresentation(getShortName(targetClass)) } /** @@ -249,28 +244,28 @@ public static String getPropertyNameRepresentation(Class targetClass) { * @param name The name to convert * @return The property name representation */ - public static String getPropertyNameRepresentation(String name) { + static String getPropertyNameRepresentation(String name) { // Strip any package from the name. - int pos = name.lastIndexOf('.'); + int pos = name.lastIndexOf('.') if (pos != -1) { - name = name.substring(pos + 1); + name = name.substring(pos + 1) } if (name.isEmpty()) { - return name; + return name } // Check whether the name begins with two upper case letters. if (name.length() > 1 && Character.isUpperCase(name.charAt(0)) && Character.isUpperCase(name.charAt(1))) { - return name; + return name } - String propertyName = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1); + String propertyName = name.substring(0, 1).toLowerCase(Locale.ENGLISH) + name.substring(1) if (propertyName.indexOf(' ') > -1) { - propertyName = propertyName.replaceAll("\\s", ""); + propertyName = propertyName.replaceAll('\\s', '') } - return propertyName; + return propertyName } /** @@ -279,8 +274,8 @@ public static String getPropertyNameRepresentation(String name) { * @param name The lower case hyphen separated name * @return The property name equivalent */ - public static String getPropertyNameForLowerCaseHyphenSeparatedName(String name) { - return getPropertyName(getClassNameForLowerCaseHyphenSeparatedName(name)); + static String getPropertyNameForLowerCaseHyphenSeparatedName(String name) { + return getPropertyName(getClassNameForLowerCaseHyphenSeparatedName(name)) } /** @@ -289,8 +284,8 @@ public static String getPropertyNameForLowerCaseHyphenSeparatedName(String name) * @param targetClass The class to get a short name for * @return The short name of the class */ - public static String getShortName(Class targetClass) { - return getShortName(targetClass.getName()); + static String getShortName(Class targetClass) { + return getShortName(targetClass.getName()) } /** @@ -299,12 +294,12 @@ public static String getShortName(Class targetClass) { * @param className The class name to get a short name for * @return The short name of the class */ - public static String getShortName(String className) { - int i = className.lastIndexOf("."); + static String getShortName(String className) { + int i = className.lastIndexOf('.') if (i > -1) { - className = className.substring(i + 1, className.length()); + className = className.substring(i + 1, className.length()) } - return className; + return className } /** @@ -313,13 +308,13 @@ public static String getShortName(String className) { * @param className The class name to get the package prefix for * @return The package prefix of the class */ - public static String getPackageName(String className) { - int i = className.lastIndexOf("."); - String packageName = ""; + static String getPackageName(String className) { + int i = className.lastIndexOf('.') + String packageName = '' if (i > -1) { - packageName = className.substring(0, i); + packageName = className.substring(0, i) } - return packageName; + return packageName } /** @@ -329,8 +324,8 @@ public static String getPackageName(String className) { * @param clazz The class to convert * @return The script name representation */ - public static String getScriptName(Class clazz) { - return clazz == null ? null : getScriptName(clazz.getName()); + static String getScriptName(Class clazz) { + return clazz == null ? null : getScriptName(clazz.getName()) } /** @@ -340,15 +335,15 @@ public static String getScriptName(Class clazz) { * @param name The class name to convert. * @return The script name representation. */ - public static String getScriptName(String name) { + static String getScriptName(String name) { if (name == null) { - return null; + return null } - if (name.endsWith(".groovy")) { - name = name.substring(0, name.length() - 7); + if (name.endsWith('.groovy')) { + name = name.substring(0, name.length() - 7) } - return getNaturalName(name).replaceAll("\\s", "-").toLowerCase(); + return getNaturalName(name).replaceAll('\\s', '-').toLowerCase() } /** @@ -357,8 +352,8 @@ public static String getScriptName(String name) { * @param scriptName The script name * @return A class name */ - public static String getNameFromScript(String scriptName) { - return getClassNameForLowerCaseHyphenSeparatedName(scriptName); + static String getNameFromScript(String scriptName) { + return getClassNameForLowerCaseHyphenSeparatedName(scriptName) } /** @@ -372,16 +367,16 @@ public static String getNameFromScript(String scriptName) { * @throws IllegalArgumentException if the given descriptor name is * not valid, i.e. if it doesn't end with "GrailsPlugin.groovy". */ - public static String getPluginName(String descriptorName) { + static String getPluginName(String descriptorName) { if (descriptorName == null || descriptorName.length() == 0) { - return descriptorName; + return descriptorName } - if (!descriptorName.endsWith("GrailsPlugin.groovy")) { - throw new IllegalArgumentException("Plugin descriptor name is not valid: " + descriptorName); + if (!descriptorName.endsWith('GrailsPlugin.groovy')) { + throw new IllegalArgumentException('Plugin descriptor name is not valid: ' + descriptorName) } - return getScriptName(descriptorName.substring(0, descriptorName.indexOf("GrailsPlugin.groovy"))); + return getScriptName(descriptorName.substring(0, descriptorName.indexOf('GrailsPlugin.groovy'))) } /** @@ -389,52 +384,52 @@ public static String getPluginName(String descriptorName) { * @param name The property name to convert * @return The converted property name */ - public static String getNaturalName(String name) { - name = getShortName(name); - List words = new ArrayList<>(); - int i = 0; - char[] chars = name.toCharArray(); + static String getNaturalName(String name) { + name = getShortName(name) + List words = new ArrayList<>() + int i = 0 + char[] chars = name.toCharArray() for (int j = 0; j < chars.length; j++) { - char c = chars[j]; - String w; + char c = chars[j] + String w if (i >= words.size()) { - w = ""; - words.add(i, w); + w = '' + words.add(i, w) } else { - w = words.get(i); + w = words.get(i) } if (Character.isLowerCase(c) || Character.isDigit(c)) { if (Character.isLowerCase(c) && w.length() == 0) { - c = Character.toUpperCase(c); + c = Character.toUpperCase(c) } else if (w.length() > 1 && Character.isUpperCase(w.charAt(w.length() - 1))) { - w = ""; - words.add(++i, w); + w = '' + words.add(++i, w) } - words.set(i, w + c); + words.set(i, w + c) } else if (Character.isUpperCase(c)) { if ((i == 0 && w.length() == 0) || (Character.isUpperCase(w.charAt(w.length() - 1)) && Character.isUpperCase(chars[j - 1]))) { - words.set(i, w + c); + words.set(i, w + c) } else { - words.add(++i, String.valueOf(c)); + words.add(++i, String.valueOf(c)) } } } - StringBuilder buf = new StringBuilder(); + StringBuilder buf = new StringBuilder() for (Iterator j = words.iterator(); j.hasNext();) { - String word = j.next(); - buf.append(word); + String word = j.next() + buf.append(word) if (j.hasNext()) { - buf.append(' '); + buf.append(' ') } } - return buf.toString(); + return buf.toString() } /** @@ -449,8 +444,8 @@ else if (Character.isUpperCase(c)) { * @return true if the string is null, or * blank. */ - public static boolean isBlank(String str) { - return str == null || str.trim().length() == 0; + static boolean isBlank(String str) { + return str == null || str.trim().length() == 0 } /** @@ -458,10 +453,10 @@ public static boolean isBlank(String str) { * @param object The object * @return The property name convention */ - public static String getPropertyNameConvention(Object object) { - String suffix = ""; + static String getPropertyNameConvention(Object object) { + String suffix = '' - return getPropertyNameConvention(object, suffix); + return getPropertyNameConvention(object, suffix) } /** @@ -470,15 +465,15 @@ public static String getPropertyNameConvention(Object object) { * @param packageName The name of the package * @return True if it is valid */ - public static boolean isValidJavaPackage(String packageName) { - if (isBlank(packageName)) return false; - final String[] parts = packageName.split("\\."); - for (String part : parts) { + static boolean isValidJavaPackage(String packageName) { + if (isBlank(packageName)) return false + final String[] parts = packageName.split('\\.') + for (String part in parts) { if (!isValidJavaIdentifier(part)) { - return false; + return false } } - return true; + return true } /** @@ -487,21 +482,21 @@ public static boolean isValidJavaPackage(String packageName) { * @param name The name * @return True if it is */ - public static boolean isValidJavaIdentifier(String name) { - if (isBlank(name)) return false; + static boolean isValidJavaIdentifier(String name) { + if (isBlank(name)) return false - final char[] chars = name.toCharArray(); + final char[] chars = name.toCharArray() if (!Character.isJavaIdentifierStart(chars[0])) { - return false; + return false } - for (char c : chars) { + for (char c in chars) { if (!Character.isJavaIdentifierPart(c)) { - return false; + return false } } - return true; + return true } /** @@ -510,46 +505,46 @@ public static boolean isValidJavaIdentifier(String name) { * @param suffix The suffix to append to the name. * @return The property name convention */ - public static String getPropertyNameConvention(Object object, String suffix) { + static String getPropertyNameConvention(Object object, String suffix) { if (object != null) { - Class type = object.getClass(); + Class type = object.getClass() if (type.isArray()) { - return getPropertyName(type.getComponentType()) + suffix + "Array"; + return getPropertyName(type.getComponentType()) + suffix + 'Array' } if (object instanceof Collection) { - Collection coll = (Collection) object; + Collection coll = (Collection) object if (coll.isEmpty()) { - return "emptyCollection"; + return 'emptyCollection' } - Object first = coll.iterator().next(); + Object first = coll.iterator().next() if (coll instanceof List) { - return getPropertyName(first.getClass()) + suffix + "List"; + return getPropertyName(first.getClass()) + suffix + 'List' } if (coll instanceof Set) { - return getPropertyName(first.getClass()) + suffix + "Set"; + return getPropertyName(first.getClass()) + suffix + 'Set' } - return getPropertyName(first.getClass()) + suffix + "Collection"; + return getPropertyName(first.getClass()) + suffix + 'Collection' } if (object instanceof Map) { - Map map = (Map) object; + Map map = (Map) object if (map.isEmpty()) { - return "emptyMap"; + return 'emptyMap' } - Object entry = map.values().iterator().next(); + Object entry = map.values().iterator().next() if (entry != null) { - return getPropertyName(entry.getClass()) + suffix + "Map"; + return getPropertyName(entry.getClass()) + suffix + 'Map' } } else { - return getPropertyName(object.getClass()) + suffix; + return getPropertyName(object.getClass()) + suffix } } - return null; + return null } /** @@ -559,8 +554,8 @@ public static String getPropertyNameConvention(Object object, String suffix) { * @param getterName The getter name * @return The property name equivalent */ - public static String getPropertyForGetter(String getterName) { - return getPropertyForGetter(getterName, boolean.class); + static String getPropertyForGetter(String getterName) { + return getPropertyForGetter(getterName, boolean.class) } /** @@ -571,8 +566,8 @@ public static String getPropertyForGetter(String getterName) { * @param returnType The type the method returns * @return The property name equivalent */ - public static String getPropertyForGetter(String getterName, Class returnType) { - return getPropertyForGetter(getterName, returnType.getName()); + static String getPropertyForGetter(String getterName, Class returnType) { + return getPropertyForGetter(getterName, returnType.getName()) } /** @@ -583,18 +578,18 @@ public static String getPropertyForGetter(String getterName, Class returnType) { * @param returnType The type the method returns * @return The property name equivalent */ - public static String getPropertyForGetter(String getterName, String returnType) { - if (getterName == null || getterName.length() == 0) return null; + static String getPropertyForGetter(String getterName, String returnType) { + if (getterName == null || getterName.length() == 0) return null - if (getterName.startsWith("get")) { - String prop = getterName.substring(3); - return convertValidPropertyMethodSuffix(prop); + if (getterName.startsWith('get')) { + String prop = getterName.substring(3) + return convertValidPropertyMethodSuffix(prop) } - if (getterName.startsWith("is") && returnType.equals("boolean")) { - String prop = getterName.substring(2); - return convertValidPropertyMethodSuffix(prop); + if (getterName.startsWith('is') && returnType.equals('boolean')) { + String prop = getterName.substring(2) + return convertValidPropertyMethodSuffix(prop) } - return null; + return null } /** @@ -605,26 +600,26 @@ public static String getPropertyForGetter(String getterName, String returnType) * @return The property name or null */ static String convertValidPropertyMethodSuffix(String suffix) { - if (suffix.length() == 0) return null; + if (suffix.length() == 0) return null // We assume all characters are Character.isJavaIdentifierPart, but the first one may not be a valid // starting character. - if (!Character.isJavaIdentifierStart(suffix.charAt(0))) return null; + if (!Character.isJavaIdentifierStart(suffix.charAt(0))) return null if (suffix.length() == 1) { - return Character.isUpperCase(suffix.charAt(0)) ? suffix.toLowerCase() : null; + return Character.isUpperCase(suffix.charAt(0)) ? suffix.toLowerCase() : null } if (Character.isUpperCase(suffix.charAt(1))) { // "aProperty", "AProperty" - return suffix; + return suffix } if (Character.isUpperCase(suffix.charAt(0))) { - return Character.toLowerCase(suffix.charAt(0)) + suffix.substring(1); + return String.valueOf(Character.toLowerCase(suffix.charAt(0))) + suffix.substring(1) } if ('_' == suffix.charAt(0)) { - return suffix; + return suffix } - return null; + return null } /** @@ -637,8 +632,8 @@ static String convertValidPropertyMethodSuffix(String suffix) { * @deprecated use {@link #isGetter(String, Class, Class[])} instead because this method has a defect for "is.." method with Boolean return types. */ @Deprecated - public static boolean isGetter(String name, Class[] args) { - return isGetter(name, boolean.class, args); + static boolean isGetter(String name, Class[] args) { + return isGetter(name, boolean.class, args) } /** @@ -650,19 +645,19 @@ public static boolean isGetter(String name, Class[] args) { * @param args The arguments * @return true if it is a javabean property getter */ - public static boolean isGetter(String name, Class returnType, Class[] args) { - if (name == null || name.length() == 0 || args == null) return false; - if (args.length != 0) return false; + static boolean isGetter(String name, Class returnType, Class[] args) { + if (name == null || name.length() == 0 || args == null) return false + if (args.length != 0) return false - if (name.startsWith("get")) { - name = name.substring(3); - if (isPropertyMethodSuffix(name)) return true; + if (name.startsWith('get')) { + name = name.substring(3) + if (isPropertyMethodSuffix(name)) return true } - else if (name.startsWith("is") && returnType == boolean.class) { - name = name.substring(2); - if (isPropertyMethodSuffix(name)) return true; + else if (name.startsWith('is') && returnType == boolean.class) { + name = name.substring(2) + if (isPropertyMethodSuffix(name)) return true } - return false; + return false } /** @@ -706,10 +701,10 @@ else if (name.startsWith("is") && returnType == boolean.class) { * @return true if suffix indicates a property name */ protected static boolean isPropertyMethodSuffix(String suffix) { - if (suffix.length() == 0) return false; - if (!Character.isJavaIdentifierStart(suffix.charAt(0))) return false; - if (suffix.length() == 1) return Character.isUpperCase(suffix.charAt(0)); - return Character.isUpperCase(suffix.charAt(0)) || Character.isUpperCase(suffix.charAt(1)); + if (suffix.length() == 0) return false + if (!Character.isJavaIdentifierStart(suffix.charAt(0))) return false + if (suffix.length() == 1) return Character.isUpperCase(suffix.charAt(0)) + return Character.isUpperCase(suffix.charAt(0)) || Character.isUpperCase(suffix.charAt(1)) } /** @@ -719,13 +714,14 @@ protected static boolean isPropertyMethodSuffix(String suffix) { * @param setterName The setter name, must be null or empty or a valid identifier name * @return The property name equivalent */ - public static String getPropertyForSetter(String setterName) { - if (setterName == null || setterName.length() == 0) return null; + static String getPropertyForSetter(String setterName) { + if (setterName == null || setterName.length() == 0) return null - if (setterName.startsWith("set")) { - String prop = setterName.substring(3); - return convertValidPropertyMethodSuffix(prop); + if (setterName.startsWith('set')) { + String prop = setterName.substring(3) + return convertValidPropertyMethodSuffix(prop) } - return null; + return null } + } diff --git a/grails-gradle/model/src/main/groovy/grails/util/Holder.java b/grails-gradle/model/src/main/groovy/grails/util/Holder.groovy similarity index 65% rename from grails-gradle/model/src/main/groovy/grails/util/Holder.java rename to grails-gradle/model/src/main/groovy/grails/util/Holder.groovy index f5696b1e187..dd0c74b2f6f 100644 --- a/grails-gradle/model/src/main/groovy/grails/util/Holder.java +++ b/grails-gradle/model/src/main/groovy/grails/util/Holder.groovy @@ -16,68 +16,71 @@ * specific language governing permissions and limitations * under the License. */ -package grails.util; +package grails.util -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentHashMap + +import groovy.transform.CompileStatic /** * @author Burt Beckwith * @since 2.0 */ -public class Holder { +@CompileStatic +class Holder { - private Map instances = new ConcurrentHashMap<>(); + private Map instances = new ConcurrentHashMap<>() // TODO remove mappedOnly and singleton - private T singleton; - private String name; + private T singleton + private String name - public Holder(String name) { - this.name = name; + Holder(String name) { + this.name = name } - public T get() { - return get(false); + T get() { + return get(false) } - public T get(boolean mappedOnly) { - T t = instances.get(getClassLoaderId()); + T get(boolean mappedOnly) { + T t = instances.get(getClassLoaderId()) if (t != null) { - return t; + return t } - t = lookupSecondary(); + t = lookupSecondary() if (t != null) { - return t; + return t } // t = instances.get(System.identityHashCode(getClass().getClassLoader())); if (!mappedOnly) { - t = singleton; + t = singleton } - return t; + return t } protected T lookupSecondary() { // override in subclass if needed - return null; + return null } - public void set(T t) { - int id = getClassLoaderId(); - int thisClassLoaderId = System.identityHashCode(getClass().getClassLoader()); + void set(T t) { + int id = getClassLoaderId() + int thisClassLoaderId = System.identityHashCode(getClass().getClassLoader()) if (t == null) { - instances.remove(id); - instances.remove(thisClassLoaderId); + instances.remove(id) + instances.remove(thisClassLoaderId) } else { - instances.put(id, t); - instances.put(thisClassLoaderId, t); + instances.put(id, t) + instances.put(thisClassLoaderId, t) } - singleton = t; + singleton = t } private int getClassLoaderId() { - return Environment.isWarDeployed() ? System.identityHashCode(Thread.currentThread().getContextClassLoader()) : System.identityHashCode(getClass().getClassLoader()); + return Environment.isWarDeployed() ? System.identityHashCode(Thread.currentThread().getContextClassLoader()) : System.identityHashCode(getClass().getClassLoader()) } + } diff --git a/grails-gradle/model/src/main/groovy/grails/util/SupplierUtil.java b/grails-gradle/model/src/main/groovy/grails/util/SupplierUtil.groovy similarity index 64% rename from grails-gradle/model/src/main/groovy/grails/util/SupplierUtil.java rename to grails-gradle/model/src/main/groovy/grails/util/SupplierUtil.groovy index 9761c0e191d..75486764af8 100644 --- a/grails-gradle/model/src/main/groovy/grails/util/SupplierUtil.java +++ b/grails-gradle/model/src/main/groovy/grails/util/SupplierUtil.groovy @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package grails.util; +package grails.util -import java.util.Optional; -import java.util.function.Supplier; +import java.util.function.Supplier + +import groovy.transform.CompileStatic /** * Helper methods for dealing with {@link Supplier}. @@ -27,7 +28,8 @@ * @author James Kleeh * @since 1.0 */ -public class SupplierUtil { +@CompileStatic +class SupplierUtil { /** * Caches the result of supplier in a thread safe manner. @@ -36,24 +38,24 @@ public class SupplierUtil { * @param The type of result * @return A new supplier that will cache the result */ - public static Supplier memoized(Supplier actual) { - return new Supplier<>() { - Supplier delegate = this::initialize; - boolean initialized; + static Supplier memoized(Supplier actual) { + return new Supplier() { + Supplier delegate = { -> initialize() } as Supplier + boolean initialized - public T get() { - return delegate.get(); + T get() { + return delegate.get() } private synchronized T initialize() { if (!initialized) { - T value = actual.get(); - delegate = () -> value; - initialized = true; + T value = actual.get() + delegate = { -> value } as Supplier + initialized = true } - return delegate.get(); + return delegate.get() } - }; + } } /** @@ -64,31 +66,32 @@ private synchronized T initialize() { * @param The type of result * @return A new supplier that will cache the result */ - public static Supplier memoizedNonEmpty(Supplier actual) { - return new Supplier<>() { - Supplier delegate = this::initialize; - boolean initialized; + static Supplier memoizedNonEmpty(Supplier actual) { + return new Supplier() { + Supplier delegate = { -> initialize() } as Supplier + boolean initialized - public T get() { - return delegate.get(); + T get() { + return delegate.get() } private synchronized T initialize() { if (!initialized) { - T value = actual.get(); + T value = actual.get() if (value == null) { - return null; + return null } if (value instanceof Optional) { if (!((Optional) value).isPresent()) { - return value; + return value } } - delegate = () -> value; - initialized = true; + delegate = { -> value } as Supplier + initialized = true } - return delegate.get(); + return delegate.get() } - }; + } } + } diff --git a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/CommandLine.java b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/CommandLine.groovy similarity index 62% rename from grails-gradle/model/src/main/groovy/org/grails/build/parsing/CommandLine.java rename to grails-gradle/model/src/main/groovy/org/grails/build/parsing/CommandLine.groovy index d37fe57888d..be6b0903a93 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/CommandLine.java +++ b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/CommandLine.groovy @@ -16,11 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.build.parsing; - -import java.util.List; -import java.util.Map; -import java.util.Properties; +package org.grails.build.parsing /** * Represents the parsed command line options. @@ -28,94 +24,95 @@ * @author Graeme Rocher * @since 2.0 */ -public interface CommandLine { - - String DEBUG_FORK = "debug-fork"; - String OFFLINE_ARGUMENT = "offline"; - String VERBOSE_ARGUMENT = "verbose"; - String STACKTRACE_ARGUMENT = "stacktrace"; - String AGENT_ARGUMENT = "reloading"; - String VERSION_ARGUMENT = "version"; - String REFRESH_DEPENDENCIES_ARGUMENT = "refresh-dependencies"; - String HELP_ARGUMENT = "help"; - String NOANSI_ARGUMENT = "plain-output"; - String NON_INTERACTIVE_ARGUMENT = "non-interactive"; +interface CommandLine { + + String DEBUG_FORK = 'debug-fork' + String OFFLINE_ARGUMENT = 'offline' + String VERBOSE_ARGUMENT = 'verbose' + String STACKTRACE_ARGUMENT = 'stacktrace' + String AGENT_ARGUMENT = 'reloading' + String VERSION_ARGUMENT = 'version' + String REFRESH_DEPENDENCIES_ARGUMENT = 'refresh-dependencies' + String HELP_ARGUMENT = 'help' + String NOANSI_ARGUMENT = 'plain-output' + String NON_INTERACTIVE_ARGUMENT = 'non-interactive' /** * @return The environment specified */ - String getEnvironment(); + String getEnvironment() /** * @return Whether the environment is user specified */ - boolean isEnvironmentSet(); + boolean isEnvironmentSet() /** * @return The command name specified */ - String getCommandName(); + String getCommandName() /** * @return The remaining arguments after the command name */ - List getRemainingArgs(); + List getRemainingArgs() /** * @return The remaining arguments as an array */ - String[] getRemainingArgsArray(); + String[] getRemainingArgsArray() /** * @return The system properties specified */ - Properties getSystemProperties(); + Properties getSystemProperties() /** * @param name The name of the option * @return Whether the given option is specified */ - boolean hasOption(String name); + boolean hasOption(String name) /** * The value of an option * @param name The option * @return The value */ - Object optionValue(String name); + Object optionValue(String name) /** * @return The last specified option */ - Map.Entry lastOption(); + Map.Entry lastOption() /** * @return The remaining args as one big string */ - String getRemainingArgsString(); + String getRemainingArgsString() /** * @return The remaining args as one big string without undeclared options */ - String getRemainingArgsWithOptionsString(); + String getRemainingArgsWithOptionsString() /** * @return The remaining args separated by the line separator char */ - String getRemainingArgsLineSeparated(); + String getRemainingArgsLineSeparated() - Map getUndeclaredOptions(); + Map getUndeclaredOptions() - String lookupEnvironmentForCommand(); + String lookupEnvironmentForCommand() - void setCommand(String scriptName); + void setCommand(String scriptName) /** * Parses a new {@link CommandLine} instance that combines this instance with the given arguments * @param args The arguments * @return A new {@link CommandLine} instance */ - CommandLine parseNew(String[] args); + CommandLine parseNew(String[] args) + + String[] getRawArguments() - String[] getRawArguments(); } diff --git a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/CommandLineParser.java b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/CommandLineParser.groovy similarity index 52% rename from grails-gradle/model/src/main/groovy/org/grails/build/parsing/CommandLineParser.java rename to grails-gradle/model/src/main/groovy/org/grails/build/parsing/CommandLineParser.groovy index 0e4d522e0d0..f36c85a180c 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/CommandLineParser.java +++ b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/CommandLineParser.groovy @@ -16,14 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.build.parsing; +package org.grails.build.parsing -import java.util.ArrayList; -import java.util.HashMap; -import java.util.Map; -import java.util.StringTokenizer; +import groovy.transform.CompileStatic -import grails.util.Environment; +import grails.util.Environment /** * Command line parser that parses arguments to the command line. Written as a @@ -35,27 +32,28 @@ * @author Graeme Rocher * @since 2.0 */ -public class CommandLineParser { +@CompileStatic +class CommandLineParser { - static Map ENV_ARGS = new HashMap<>(); - static Map DEFAULT_ENVS = new HashMap<>(); - private static CommandLine CURRENT = null; - private static final String DEFAULT_PADDING = " "; + static Map ENV_ARGS = new HashMap<>() + static Map DEFAULT_ENVS = new HashMap<>() + private static CommandLine CURRENT = null + private static final String DEFAULT_PADDING = ' ' static { - ENV_ARGS.put("dev", Environment.DEVELOPMENT.getName()); - ENV_ARGS.put("prod", Environment.PRODUCTION.getName()); - ENV_ARGS.put("test", Environment.TEST.getName()); - DEFAULT_ENVS.put("war", Environment.PRODUCTION.getName()); - DEFAULT_ENVS.put("test-app", Environment.TEST.getName()); + ENV_ARGS.put('dev', Environment.DEVELOPMENT.getName()) + ENV_ARGS.put('prod', Environment.PRODUCTION.getName()) + ENV_ARGS.put('test', Environment.TEST.getName()) + DEFAULT_ENVS.put('war', Environment.PRODUCTION.getName()) + DEFAULT_ENVS.put('test-app', Environment.TEST.getName()) } - private Map declaredOptions = new HashMap<>(); - private int longestOptionNameLength = 0; - private String usageMessage; + private Map declaredOptions = new HashMap<>() + private int longestOptionNameLength = 0 + private String usageMessage - public static CommandLine getCurrentCommandLine() { - return CURRENT; + static CommandLine getCurrentCommandLine() { + return CURRENT } /** @@ -64,12 +62,12 @@ public static CommandLine getCurrentCommandLine() { * @param name The name of the option * @param description The description */ - public void addOption(String name, String description) { - int length = name.length(); + void addOption(String name, String description) { + int length = name.length() if (length > longestOptionNameLength) { - longestOptionNameLength = length; + longestOptionNameLength = length } - declaredOptions.put(name, new Option(name, description)); + declaredOptions.put(name, new Option(name, description)) } /** @@ -78,10 +76,10 @@ public void addOption(String name, String description) { * @param string The string * @return The command line */ - public CommandLine parseString(String string) { + CommandLine parseString(String string) { // Steal ants implementation for argument splitting. Handles quoted arguments with " or '. // Doesn't handle escape sequences with \ - return parse(translateCommandline(string)); + return parse(translateCommandline(string)) } /** @@ -90,65 +88,65 @@ public CommandLine parseString(String string) { * @return the command line broken into strings. * An empty or null toProcess parameter results in a zero sized array. */ - public static String[] translateCommandline(String toProcess) { + static String[] translateCommandline(String toProcess) { if (toProcess == null || toProcess.length() == 0) { //no command? no string - return new String[0]; + return new String[0] } // parse with a simple finite state machine - final int normal = 0; - final int inQuote = 1; - final int inDoubleQuote = 2; - int state = normal; - final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true); - final ArrayList result = new ArrayList<>(); - final StringBuilder current = new StringBuilder(); - boolean lastTokenHasBeenQuoted = false; + final int normal = 0 + final int inQuote = 1 + final int inDoubleQuote = 2 + int state = normal + final StringTokenizer tok = new StringTokenizer(toProcess, "\"\' ", true) + final ArrayList result = new ArrayList<>() + final StringBuilder current = new StringBuilder() + boolean lastTokenHasBeenQuoted = false while (tok.hasMoreTokens()) { - String nextTok = tok.nextToken(); + String nextTok = tok.nextToken() switch (state) { case inQuote: if ("\'".equals(nextTok)) { - lastTokenHasBeenQuoted = true; - state = normal; + lastTokenHasBeenQuoted = true + state = normal } else { - current.append(nextTok); + current.append(nextTok) } - break; + break case inDoubleQuote: - if ("\"".equals(nextTok)) { - lastTokenHasBeenQuoted = true; - state = normal; + if ('"'.equals(nextTok)) { + lastTokenHasBeenQuoted = true + state = normal } else { - current.append(nextTok); + current.append(nextTok) } - break; + break default: if ("\'".equals(nextTok)) { - state = inQuote; - } else if ("\"".equals(nextTok)) { - state = inDoubleQuote; - } else if (" ".equals(nextTok)) { + state = inQuote + } else if ('"'.equals(nextTok)) { + state = inDoubleQuote + } else if (' '.equals(nextTok)) { if (lastTokenHasBeenQuoted || current.length() != 0) { - result.add(current.toString()); - current.setLength(0); + result.add(current.toString()) + current.setLength(0) } } else { - current.append(nextTok); + current.append(nextTok) } - lastTokenHasBeenQuoted = false; - break; + lastTokenHasBeenQuoted = false + break } } if (lastTokenHasBeenQuoted || current.length() != 0) { - result.add(current.toString()); + result.add(current.toString()) } if (state == inQuote || state == inDoubleQuote) { - throw new ParseException("unbalanced quotes in " + toProcess); + throw new ParseException('unbalanced quotes in ' + toProcess) } - return result.toArray(new String[result.size()]); + return result.toArray(new String[result.size()]) } /** @@ -158,14 +156,14 @@ public static String[] translateCommandline(String toProcess) { * @param args The string * @return The command line */ - public CommandLine parseString(String commandName, String args) { + CommandLine parseString(String commandName, String args) { // Steal ants implementation for argument splitting. Handles quoted arguments with " or '. // Doesn't handle escape sequences with \ - String[] argArray = translateCommandline(args); - DefaultCommandLine cl = createCommandLine(); - cl.setCommandName(commandName); - parseInternal(cl, argArray, false); - return cl; + String[] argArray = translateCommandline(args) + DefaultCommandLine cl = createCommandLine() + cl.setCommandName(commandName) + parseInternal(cl, argArray, false) + return cl } /** @@ -176,45 +174,45 @@ public CommandLine parseString(String commandName, String args) { * @param args The arguments * @return The command line state */ - public CommandLine parse(String... args) { - DefaultCommandLine cl = createCommandLine(); - return parse(cl, args); + CommandLine parse(String... args) { + DefaultCommandLine cl = createCommandLine() + return parse(cl, args) } - public CommandLine parse(DefaultCommandLine cl, String[] args) { - parseInternal(cl, args, true); - return cl; + CommandLine parse(DefaultCommandLine cl, String[] args) { + parseInternal(cl, args, true) + return cl } private void parseInternal(DefaultCommandLine cl, String[] args, boolean firstArgumentIsCommand) { - cl.setRawArguments(args); - String lastWasOption = null; - for (String arg : args) { - if (arg == null) continue; - String trimmed = arg.trim(); + cl.setRawArguments(args) + String lastWasOption = null + for (String arg in args) { + if (arg == null) continue + String trimmed = arg.trim() if (trimmed != null && trimmed.length() > 0) { if (trimmed.charAt(0) == '"' && trimmed.charAt(trimmed.length() - 1) == '"') { - trimmed = trimmed.substring(1, trimmed.length() - 1); + trimmed = trimmed.substring(1, trimmed.length() - 1) } if (trimmed.charAt(0) == '-') { - lastWasOption = processOption(cl, trimmed); + lastWasOption = processOption(cl, trimmed) } else { if (lastWasOption != null) { - cl.addUndeclaredOption(lastWasOption, trimmed); - lastWasOption = null; - continue; + cl.addUndeclaredOption(lastWasOption, trimmed) + lastWasOption = null + continue } if (firstArgumentIsCommand && ENV_ARGS.containsKey(trimmed)) { - cl.setEnvironment(ENV_ARGS.get(trimmed)); + cl.setEnvironment(ENV_ARGS.get(trimmed)) } else { if (firstArgumentIsCommand) { - cl.setCommandName(trimmed); - firstArgumentIsCommand = false; + cl.setCommandName(trimmed) + firstArgumentIsCommand = false } else { - cl.addRemainingArg(trimmed); + cl.addRemainingArg(trimmed) } } } @@ -222,74 +220,75 @@ private void parseInternal(DefaultCommandLine cl, String[] args, boolean firstAr } } - public String getOptionsHelpMessage() { - String ls = System.getProperty("line.separator"); - usageMessage = "Available options:"; - StringBuilder sb = new StringBuilder(usageMessage); - sb.append(ls); - for (Option option : declaredOptions.values()) { - String name = option.getName(); - int extraPadding = longestOptionNameLength - name.length(); - sb.append(" -").append(name); + String getOptionsHelpMessage() { + String ls = System.getProperty('line.separator') + usageMessage = 'Available options:' + StringBuilder sb = new StringBuilder(usageMessage) + sb.append(ls) + for (Option option in declaredOptions.values()) { + String name = option.getName() + int extraPadding = longestOptionNameLength - name.length() + sb.append(' -').append(name) for (int i = 0; i < extraPadding; i++) { - sb.append(' '); + sb.append(' ') } - sb.append(DEFAULT_PADDING).append(option.getDescription()).append(ls); + sb.append(DEFAULT_PADDING).append(option.getDescription()).append(ls) } - return sb.toString(); + return sb.toString() } protected DefaultCommandLine createCommandLine() { - DefaultCommandLine defaultCommandLine = new DefaultCommandLine(); - CURRENT = defaultCommandLine; - return defaultCommandLine; + DefaultCommandLine defaultCommandLine = new DefaultCommandLine() + CURRENT = defaultCommandLine + return defaultCommandLine } protected String processOption(DefaultCommandLine cl, String arg) { if (arg.length() < 2) { - return null; + return null } - if (arg.charAt(1) == 'D' && arg.contains("=")) { - processSystemArg(cl, arg); - return null; + if (arg.charAt(1) == 'D' && arg.contains('=')) { + processSystemArg(cl, arg) + return null } - arg = (arg.charAt(1) == '-' ? arg.substring(2, arg.length()) : arg.substring(1, arg.length())).trim(); + arg = (arg.charAt(1) == '-' ? arg.substring(2, arg.length()) : arg.substring(1, arg.length())).trim() - if (arg.contains("=")) { - String[] split = arg.split("="); - String name = split[0].trim(); - validateOptionName(name); - String value = split[1].trim(); + if (arg.contains('=')) { + String[] split = arg.split('=') + String name = split[0].trim() + validateOptionName(name) + String value = split[1].trim() if (declaredOptions.containsKey(name)) { - cl.addDeclaredOption(name, declaredOptions.get(name), value); + cl.addDeclaredOption(name, declaredOptions.get(name), value) } else { - cl.addUndeclaredOption(name, value); + cl.addUndeclaredOption(name, value) } - return null; + return null } - validateOptionName(arg); + validateOptionName(arg) if (declaredOptions.containsKey(arg)) { - cl.addDeclaredOption(arg, declaredOptions.get(arg)); + cl.addDeclaredOption(arg, declaredOptions.get(arg)) } else { - cl.addUndeclaredOption(arg); + cl.addUndeclaredOption(arg) } - return arg; + return arg } private void validateOptionName(String name) { - if (name.contains(" ")) throw new ParseException("Invalid argument: " + name); + if (name.contains(' ')) throw new ParseException('Invalid argument: ' + name) } protected void processSystemArg(DefaultCommandLine cl, String arg) { - int i = arg.indexOf("="); - String name = arg.substring(2, i); - String value = arg.substring(i + 1, arg.length()); - cl.addSystemProperty(name, value); + int i = arg.indexOf('=') + String name = arg.substring(2, i) + String value = arg.substring(i + 1, arg.length()) + cl.addSystemProperty(name, value) } + } diff --git a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy new file mode 100644 index 00000000000..c2e16df9fd5 --- /dev/null +++ b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package org.grails.build.parsing + +import groovy.transform.CompileStatic +import groovy.transform.PackageScope + +import grails.util.Environment + +/** + * Implementation of the {@link CommandLine} interface. + * + * @author Graeme Rocher + * @since 2.0 + */ +@CompileStatic +class DefaultCommandLine implements CommandLine { + + @PackageScope Properties systemProperties = new Properties() + @PackageScope LinkedHashMap undeclaredOptions = new LinkedHashMap<>() + @PackageScope LinkedHashMap declaredOptions = new LinkedHashMap<>() + @PackageScope List remainingArgs = new ArrayList<>() + private String environment + private String commandName + private String[] rawArguments + + void addDeclaredOption(String name, Option option) { + addDeclaredOption(name, option, Boolean.TRUE) + } + + void addUndeclaredOption(String option) { + undeclaredOptions.put(option, Boolean.TRUE) + } + + void addUndeclaredOption(String option, Object value) { + undeclaredOptions.put(option, value) + } + + void addDeclaredOption(String name, Option option, Object value) { + SpecifiedOption so = new SpecifiedOption() + so.option = option + so.value = value + + declaredOptions.put(name, so) + } + + @Override + CommandLine parseNew(String[] args) { + DefaultCommandLine defaultCommandLine = new DefaultCommandLine() + defaultCommandLine.systemProperties.putAll(systemProperties) + defaultCommandLine.undeclaredOptions.putAll(undeclaredOptions) + defaultCommandLine.declaredOptions.putAll(declaredOptions) + CommandLineParser parser = new CommandLineParser() + return parser.parse(defaultCommandLine, args) + } + + void setEnvironment(String environment) { + this.environment = environment + System.setProperty(Environment.KEY, environment) + } + + void setCommand(String name) { + commandName = name + } + + String getEnvironment() { + boolean useDefaultEnv = environment == null + String env + if (useDefaultEnv && commandName != null) { + env = lookupEnvironmentForCommand() + } + else { + String fallbackEnv = System.getProperty(Environment.KEY) != null ? System.getProperty(Environment.KEY) : Environment.DEVELOPMENT.getName() + env = environment != null ? environment : fallbackEnv + } + + System.setProperty(Environment.KEY, env) + System.setProperty(Environment.DEFAULT, String.valueOf(useDefaultEnv)) + + return env + } + + String lookupEnvironmentForCommand() { + String fallbackEnv = System.getProperty(Environment.KEY) != null ? System.getProperty(Environment.KEY) : Environment.DEVELOPMENT.getName() + String env = CommandLineParser.DEFAULT_ENVS.get(commandName) + return env == null ? fallbackEnv : env + } + + boolean isEnvironmentSet() { + return environment != null + } + + void setCommandName(String cmd) { + if (REFRESH_DEPENDENCIES_ARGUMENT.equals(cmd)) { + addUndeclaredOption(REFRESH_DEPENDENCIES_ARGUMENT) + } + commandName = cmd + } + + String getCommandName() { + return commandName + } + + void addRemainingArg(String arg) { + remainingArgs.add(arg) + } + + List getRemainingArgs() { + return remainingArgs + } + + String[] getRemainingArgsArray() { + return remainingArgs.toArray(new String[remainingArgs.size()]) + } + + Properties getSystemProperties() { + return systemProperties + } + + boolean hasOption(String name) { + return declaredOptions.containsKey(name) || undeclaredOptions.containsKey(name) + } + + Object optionValue(String name) { + if (declaredOptions.containsKey(name)) { + SpecifiedOption specifiedOption = declaredOptions.get(name) + return specifiedOption.value + } + if (undeclaredOptions.containsKey(name)) { + return undeclaredOptions.get(name) + } + return null + } + + @Override + Map.Entry lastOption() { + final Iterator> i = undeclaredOptions.entrySet().iterator() + while (i.hasNext()) { + Map.Entry next = i.next() + if (!i.hasNext()) { + return next + } + } + return null + } + + String getRemainingArgsString() { + return remainingArgsToString(' ', false) + } + + @Override + String getRemainingArgsWithOptionsString() { + return remainingArgsToString(' ', true) + } + + String getRemainingArgsLineSeparated() { + return remainingArgsToString('\n', false) + } + + private String remainingArgsToString(String separator, boolean includeOptions) { + StringBuilder sb = new StringBuilder() + String sep = '' + List args = new ArrayList<>(remainingArgs) + if (includeOptions) { + for (Map.Entry entry : undeclaredOptions.entrySet()) { + if (entry.getValue() instanceof Boolean && ((Boolean) entry.getValue())) { + args.add('-' + entry.getKey()) + } + else { + args.add('-' + entry.getKey() + '=' + entry.getValue()) + } + } + } + for (String arg in args) { + sb.append(sep).append(arg) + sep = separator + } + return sb.toString() + } + + Map getUndeclaredOptions() { + return undeclaredOptions + } + + void addSystemProperty(String name, String value) { + if (Environment.KEY.equals(name)) { + setEnvironment(value) + } + systemProperties.put(name, value) + } + + void setRawArguments(String[] args) { + this.rawArguments = args + } + + @Override + String[] getRawArguments() { + return rawArguments + } + + static class SpecifiedOption { + private Option option + private Object value + + Option getOption() { + return option + } + + Object getValue() { + return value + } + } + +} diff --git a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.java b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.java deleted file mode 100644 index a4527054f56..00000000000 --- a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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 - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * 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. - */ -package org.grails.build.parsing; - -import java.util.ArrayList; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import grails.util.Environment; - -/** - * Implementation of the {@link CommandLine} interface. - * - * @author Graeme Rocher - * @since 2.0 - */ -public class DefaultCommandLine implements CommandLine { - - Properties systemProperties = new Properties(); - LinkedHashMap undeclaredOptions = new LinkedHashMap<>(); - LinkedHashMap declaredOptions = new LinkedHashMap<>(); - List remainingArgs = new ArrayList<>(); - private String environment; - private String commandName; - private String[] rawArguments; - - public void addDeclaredOption(String name, Option option) { - addDeclaredOption(name, option, Boolean.TRUE); - } - - public void addUndeclaredOption(String option) { - undeclaredOptions.put(option, Boolean.TRUE); - } - - public void addUndeclaredOption(String option, Object value) { - undeclaredOptions.put(option, value); - } - - public void addDeclaredOption(String name, Option option, Object value) { - SpecifiedOption so = new SpecifiedOption(); - so.option = option; - so.value = value; - - declaredOptions.put(name, so); - } - - @Override - public CommandLine parseNew(String[] args) { - DefaultCommandLine defaultCommandLine = new DefaultCommandLine(); - defaultCommandLine.systemProperties.putAll(systemProperties); - defaultCommandLine.undeclaredOptions.putAll(undeclaredOptions); - defaultCommandLine.declaredOptions.putAll(declaredOptions); - CommandLineParser parser = new CommandLineParser(); - return parser.parse(defaultCommandLine, args); - } - - public void setEnvironment(String environment) { - this.environment = environment; - System.setProperty(Environment.KEY, environment); - } - - public void setCommand(String name) { - commandName = name; - } - - public String getEnvironment() { - boolean useDefaultEnv = environment == null; - String env; - if (useDefaultEnv && commandName != null) { - env = lookupEnvironmentForCommand(); - } - else { - String fallbackEnv = System.getProperty(Environment.KEY) != null ? System.getProperty(Environment.KEY) : Environment.DEVELOPMENT.getName(); - env = environment != null ? environment : fallbackEnv; - } - - System.setProperty(Environment.KEY, env); - System.setProperty(Environment.DEFAULT, String.valueOf(useDefaultEnv)); - - return env; - } - - public String lookupEnvironmentForCommand() { - String fallbackEnv = System.getProperty(Environment.KEY) != null ? System.getProperty(Environment.KEY) : Environment.DEVELOPMENT.getName(); - String env = CommandLineParser.DEFAULT_ENVS.get(commandName); - return env == null ? fallbackEnv : env; - } - - public boolean isEnvironmentSet() { - return environment != null; - } - - public void setCommandName(String cmd) { - if (REFRESH_DEPENDENCIES_ARGUMENT.equals(cmd)) { - addUndeclaredOption(REFRESH_DEPENDENCIES_ARGUMENT); - } - commandName = cmd; - } - - public String getCommandName() { - return commandName; - } - - public void addRemainingArg(String arg) { - remainingArgs.add(arg); - } - - public List getRemainingArgs() { - return remainingArgs; - } - - public String[] getRemainingArgsArray() { - return remainingArgs.toArray(new String[remainingArgs.size()]); - } - - public Properties getSystemProperties() { - return systemProperties; - } - - public boolean hasOption(String name) { - return declaredOptions.containsKey(name) || undeclaredOptions.containsKey(name); - } - - public Object optionValue(String name) { - if (declaredOptions.containsKey(name)) { - SpecifiedOption specifiedOption = declaredOptions.get(name); - return specifiedOption.value; - } - if (undeclaredOptions.containsKey(name)) { - return undeclaredOptions.get(name); - } - return null; - } - - @Override - public Map.Entry lastOption() { - final Iterator> i = undeclaredOptions.entrySet().iterator(); - while (i.hasNext()) { - Map.Entry next = i.next(); - if (!i.hasNext()) { - return next; - } - } - return null; - } - - public String getRemainingArgsString() { - return remainingArgsToString(" ", false); - } - - @Override - public String getRemainingArgsWithOptionsString() { - return remainingArgsToString(" ", true); - } - - public String getRemainingArgsLineSeparated() { - return remainingArgsToString("\n", false); - } - - private String remainingArgsToString(String separator, boolean includeOptions) { - StringBuilder sb = new StringBuilder(); - String sep = ""; - List args = new ArrayList<>(remainingArgs); - if (includeOptions) { - for (Map.Entry entry : undeclaredOptions.entrySet()) { - if (entry.getValue() instanceof Boolean && ((Boolean) entry.getValue())) { - args.add('-' + entry.getKey()); - } - else { - args.add('-' + entry.getKey() + '=' + entry.getValue()); - } - } - } - for (String arg : args) { - sb.append(sep).append(arg); - sep = separator; - } - return sb.toString(); - } - - public Map getUndeclaredOptions() { - return undeclaredOptions; - } - - public void addSystemProperty(String name, String value) { - if (Environment.KEY.equals(name)) { - setEnvironment(value); - } - systemProperties.put(name, value); - } - - public void setRawArguments(String[] args) { - this.rawArguments = args; - } - - @Override - public String[] getRawArguments() { - return rawArguments; - } - - public static class SpecifiedOption { - private Option option; - private Object value; - - public Option getOption() { - return option; - } - - public Object getValue() { - return value; - } - } -} diff --git a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/Option.java b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/Option.groovy similarity index 67% rename from grails-gradle/model/src/main/groovy/org/grails/build/parsing/Option.java rename to grails-gradle/model/src/main/groovy/org/grails/build/parsing/Option.groovy index ec94c1c138d..fc20727ac96 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/Option.java +++ b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/Option.groovy @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.build.parsing; +package org.grails.build.parsing + +import groovy.transform.CompileStatic /** * Represents a command line option. @@ -24,25 +26,27 @@ * @author Graeme Rocher * @since 2.0 */ -public class Option { +@CompileStatic +class Option { - private String name; - private String description; + private String name + private String description - public Option(String name, String description) { + Option(String name, String description) { if (name == null || name.length() == 0) { - throw new IllegalArgumentException("illegal option specified"); + throw new IllegalArgumentException('illegal option specified') } - this.name = name; - this.description = description == null ? "" : description; + this.name = name + this.description = description == null ? '' : description } - public String getName() { - return name; + String getName() { + return name } - public String getDescription() { - return description; + String getDescription() { + return description } + } diff --git a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/ParseException.java b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/ParseException.groovy similarity index 75% rename from grails-gradle/model/src/main/groovy/org/grails/build/parsing/ParseException.java rename to grails-gradle/model/src/main/groovy/org/grails/build/parsing/ParseException.groovy index cf8c73e9ad7..f8b9ca0f521 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/ParseException.java +++ b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/ParseException.groovy @@ -16,7 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.build.parsing; +package org.grails.build.parsing + +import groovy.transform.CompileStatic /** * Exception that is thrown in command line parsing fails. @@ -24,15 +26,17 @@ * @author Graeme Rocher * @since 2.0 */ -public class ParseException extends RuntimeException { +@CompileStatic +class ParseException extends RuntimeException { - private static final long serialVersionUID = 1; + private static final long serialVersionUID = 1 - public ParseException(String message) { - super(message); + ParseException(String message) { + super(message) } - public ParseException(Throwable cause) { - super(cause); + ParseException(Throwable cause) { + super(cause) } + } diff --git a/grails-gradle/model/src/main/groovy/org/grails/io/support/AbstractFileResolvingResource.java b/grails-gradle/model/src/main/groovy/org/grails/io/support/AbstractFileResolvingResource.groovy similarity index 66% rename from grails-gradle/model/src/main/groovy/org/grails/io/support/AbstractFileResolvingResource.java rename to grails-gradle/model/src/main/groovy/org/grails/io/support/AbstractFileResolvingResource.groovy index 443ba91fb86..9e2a2cb6e34 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/io/support/AbstractFileResolvingResource.java +++ b/grails-gradle/model/src/main/groovy/org/grails/io/support/AbstractFileResolvingResource.groovy @@ -16,15 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.io.support; +package org.grails.io.support -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.URI; -import java.net.URL; -import java.net.URLConnection; +import groovy.transform.CompileStatic /** * Abstract base class for resources which resolve URLs into File references, @@ -36,15 +30,16 @@ * @author Juergen Hoeller * @since 3.0 */ -public abstract class AbstractFileResolvingResource implements Resource { +@CompileStatic +abstract class AbstractFileResolvingResource implements Resource { /** * This implementation returns a File reference for the underlying class path * resource, provided that it refers to a file in the file system. */ - public File getFile() throws IOException { - URL url = getURL(); - return GrailsResourceUtils.getFile(url, getDescription()); + File getFile() throws IOException { + URL url = getURL() + return GrailsResourceUtils.getFile(url, getDescription()) } /** @@ -52,12 +47,12 @@ public File getFile() throws IOException { * (or jar file, in case of a resource in a jar/zip). */ protected File getFileForLastModifiedCheck() throws IOException { - URL url = getURL(); + URL url = getURL() if (GrailsResourceUtils.isJarURL(url)) { - URL actualUrl = GrailsResourceUtils.extractJarFileURL(url); - return GrailsResourceUtils.getFile(actualUrl, "Jar URL"); + URL actualUrl = GrailsResourceUtils.extractJarFileURL(url) + return GrailsResourceUtils.getFile(actualUrl, 'Jar URL') } - return getFile(); + return getFile() } /** @@ -65,7 +60,7 @@ protected File getFileForLastModifiedCheck() throws IOException { * resource, provided that it refers to a file in the file system. */ protected File getFile(URI uri) throws IOException { - return GrailsResourceUtils.getFile(uri, getDescription()); + return GrailsResourceUtils.getFile(uri, getDescription()) } /** @@ -75,93 +70,94 @@ protected File getFile(URI uri) throws IOException { * @param con the URLConnection to set the flag on */ private static void useCachesIfNecessary(URLConnection con) { - con.setUseCaches(con.getClass().getName().startsWith("JNLP")); + con.setUseCaches(con.getClass().getName().startsWith('JNLP')) } - public boolean exists() { + boolean exists() { try { - URL url = getURL(); + URL url = getURL() if (GrailsResourceUtils.isFileURL(url)) { // Proceed with file system resolution... - return getFile().exists(); + return getFile().exists() } // Try a URL connection content-length header... - URLConnection con = url.openConnection(); - useCachesIfNecessary(con); + URLConnection con = url.openConnection() + useCachesIfNecessary(con) HttpURLConnection httpCon = - (con instanceof HttpURLConnection ? (HttpURLConnection) con : null); + (con instanceof HttpURLConnection ? (HttpURLConnection) con : null) if (httpCon != null) { - httpCon.setRequestMethod("HEAD"); - int code = httpCon.getResponseCode(); + httpCon.setRequestMethod('HEAD') + int code = httpCon.getResponseCode() if (code == HttpURLConnection.HTTP_OK) { - return true; + return true } if (code == HttpURLConnection.HTTP_NOT_FOUND) { - return false; + return false } } if (con.getContentLength() >= 0) { - return true; + return true } if (httpCon != null) { // no HTTP OK status, and no content-length header: give up - httpCon.disconnect(); - return false; + httpCon.disconnect() + return false } // Fall back to stream existence: can we open the stream? - InputStream is = getInputStream(); - is.close(); - return true; + InputStream is = getInputStream() + is.close() + return true } catch (IOException ex) { - return false; + return false } } - public boolean isReadable() { + boolean isReadable() { try { - URL url = getURL(); + URL url = getURL() if (GrailsResourceUtils.isFileURL(url)) { // Proceed with file system resolution... - File file = getFile(); - return (file.canRead() && !file.isDirectory()); + File file = getFile() + return (file.canRead() && !file.isDirectory()) } - return true; + return true } catch (IOException ex) { - return false; + return false } } - public long contentLength() throws IOException { - URL url = getURL(); + long contentLength() throws IOException { + URL url = getURL() if (GrailsResourceUtils.isFileURL(url)) { // Proceed with file system resolution... - return getFile().length(); + return getFile().length() } // Try a URL connection content-length header... - URLConnection con = url.openConnection(); - useCachesIfNecessary(con); + URLConnection con = url.openConnection() + useCachesIfNecessary(con) if (con instanceof HttpURLConnection) { - ((HttpURLConnection) con).setRequestMethod("HEAD"); + ((HttpURLConnection) con).setRequestMethod('HEAD') } - return con.getContentLength(); + return con.getContentLength() } - public long lastModified() throws IOException { - URL url = getURL(); + long lastModified() throws IOException { + URL url = getURL() if (GrailsResourceUtils.isFileURL(url) || GrailsResourceUtils.isJarURL(url)) { // Proceed with file system resolution... - return getFile().lastModified(); + return getFile().lastModified() } // Try a URL connection last-modified header... - URLConnection con = url.openConnection(); - useCachesIfNecessary(con); + URLConnection con = url.openConnection() + useCachesIfNecessary(con) if (con instanceof HttpURLConnection) { - ((HttpURLConnection) con).setRequestMethod("HEAD"); + ((HttpURLConnection) con).setRequestMethod('HEAD') } - return con.getLastModified(); + return con.getLastModified() } + } diff --git a/grails-gradle/model/src/main/groovy/org/grails/io/support/ClassPathResource.java b/grails-gradle/model/src/main/groovy/org/grails/io/support/ClassPathResource.groovy similarity index 69% rename from grails-gradle/model/src/main/groovy/org/grails/io/support/ClassPathResource.java rename to grails-gradle/model/src/main/groovy/org/grails/io/support/ClassPathResource.groovy index 4a3f089a7a4..e5c3ab6bf80 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/io/support/ClassPathResource.java +++ b/grails-gradle/model/src/main/groovy/org/grails/io/support/ClassPathResource.groovy @@ -16,14 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.io.support; +package org.grails.io.support -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URL; -import java.util.Arrays; +import groovy.transform.CompileStatic /** * Resource implementation for class path resources. @@ -38,13 +33,14 @@ * @see java.lang.ClassLoader#getResourceAsStream(String) * @see java.lang.Class#getResourceAsStream(String) */ -public class ClassPathResource extends AbstractFileResolvingResource { +@CompileStatic +class ClassPathResource extends AbstractFileResolvingResource { - private final String path; + private final String path - private ClassLoader classLoader; + private ClassLoader classLoader - private Class clazz; + private Class clazz /** * Create a new ClassPathResource for ClassLoader usage. @@ -55,8 +51,8 @@ public class ClassPathResource extends AbstractFileResolvingResource { * @param path the absolute path within the class path * @see java.lang.ClassLoader#getResourceAsStream(String) */ - public ClassPathResource(String path) { - this(path, (ClassLoader) null); + ClassPathResource(String path) { + this(path, (ClassLoader) null) } /** @@ -68,13 +64,13 @@ public ClassPathResource(String path) { * or null for the thread context class loader * @see java.lang.ClassLoader#getResourceAsStream(String) */ - public ClassPathResource(String path, ClassLoader classLoader) { - String pathToUse = GrailsResourceUtils.cleanPath(path); - if (pathToUse.startsWith("/")) { - pathToUse = pathToUse.substring(1); + ClassPathResource(String path, ClassLoader classLoader) { + String pathToUse = GrailsResourceUtils.cleanPath(path) + if (pathToUse.startsWith('/')) { + pathToUse = pathToUse.substring(1) } - this.path = pathToUse; - this.classLoader = classLoader == null ? DefaultResourceLoader.getDefaultClassLoader() : classLoader; + this.path = pathToUse + this.classLoader = classLoader == null ? DefaultResourceLoader.getDefaultClassLoader() : classLoader } /** @@ -85,9 +81,9 @@ public ClassPathResource(String path, ClassLoader classLoader) { * @param clazz the class to load resources with * @see java.lang.Class#getResourceAsStream */ - public ClassPathResource(String path, Class clazz) { - this.path = GrailsResourceUtils.cleanPath(path); - this.clazz = clazz; + ClassPathResource(String path, Class clazz) { + this.path = GrailsResourceUtils.cleanPath(path) + this.clazz = clazz } /** @@ -98,23 +94,23 @@ public ClassPathResource(String path, Class clazz) { * @param clazz the class to load resources with, if any */ protected ClassPathResource(String path, ClassLoader classLoader, Class clazz) { - this.path = GrailsResourceUtils.cleanPath(path); - this.classLoader = classLoader; - this.clazz = clazz; + this.path = GrailsResourceUtils.cleanPath(path) + this.classLoader = classLoader + this.clazz = clazz } /** * Return the path for this resource (as resource path within the class path). */ - public final String getPath() { - return path; + final String getPath() { + return path } /** * Return the ClassLoader that this resource will be obtained from. */ - public final ClassLoader getClassLoader() { - return classLoader == null ? clazz.getClassLoader() : classLoader; + final ClassLoader getClassLoader() { + return classLoader == null ? clazz.getClassLoader() : classLoader } /** @@ -123,15 +119,15 @@ public final ClassLoader getClassLoader() { * @see java.lang.Class#getResource(String) */ @Override - public boolean exists() { - URL url; + boolean exists() { + URL url if (clazz == null) { - url = classLoader.getResource(path); + url = classLoader.getResource(path) } else { - url = clazz.getResource(path); + url = clazz.getResource(path) } - return url != null; + return url != null } /** @@ -139,19 +135,19 @@ public boolean exists() { * @see java.lang.ClassLoader#getResourceAsStream(String) * @see java.lang.Class#getResourceAsStream(String) */ - public InputStream getInputStream() throws IOException { - InputStream is; + InputStream getInputStream() throws IOException { + InputStream is if (clazz == null) { - is = classLoader.getResourceAsStream(path); + is = classLoader.getResourceAsStream(path) } else { - is = clazz.getResourceAsStream(path); + is = clazz.getResourceAsStream(path) } if (is == null) { throw new FileNotFoundException( - getDescription() + " cannot be opened because it does not exist"); + getDescription() + ' cannot be opened because it does not exist') } - return is; + return is } /** @@ -159,115 +155,115 @@ public InputStream getInputStream() throws IOException { * @see java.lang.ClassLoader#getResource(String) * @see java.lang.Class#getResource(String) */ - public URL getURL() throws IOException { - URL url; + URL getURL() throws IOException { + URL url if (clazz == null) { - url = classLoader.getResource(path); + url = classLoader.getResource(path) } else { - url = clazz.getResource(path); + url = clazz.getResource(path) } if (url == null) { throw new FileNotFoundException( - getDescription() + " cannot be resolved to URL because it does not exist"); + getDescription() + ' cannot be resolved to URL because it does not exist') } - return url; + return url } - public URI getURI() throws IOException { - return getFile().toURI(); + URI getURI() throws IOException { + return getFile().toURI() } /** * This implementation creates a ClassPathResource, applying the given path * relative to the path of the underlying resource of this descriptor. */ - public Resource createRelative(String relativePath) { - String pathToUse = GrailsResourceUtils.applyRelativePath(path, relativePath); - return new ClassPathResource(pathToUse, classLoader, clazz); + Resource createRelative(String relativePath) { + String pathToUse = GrailsResourceUtils.applyRelativePath(path, relativePath) + return new ClassPathResource(pathToUse, classLoader, clazz) } /** * This implementation returns the name of the file that this class path * resource refers to. */ - public String getFilename() { - return GrailsResourceUtils.getFilename(path); + String getFilename() { + return GrailsResourceUtils.getFilename(path) } /** * This implementation returns a description that includes the class path location. */ - public String getDescription() { - StringBuilder builder = new StringBuilder("class path resource ["); + String getDescription() { + StringBuilder builder = new StringBuilder('class path resource [') if (clazz != null) { - builder.append(GrailsResourceUtils.classPackageAsResourcePath(clazz)); - builder.append('/'); + builder.append(GrailsResourceUtils.classPackageAsResourcePath(clazz)) + builder.append('/') } - builder.append(path); - builder.append(']'); - return builder.toString(); + builder.append(path) + builder.append(']') + return builder.toString() } /** * This implementation compares the underlying class path locations. */ @Override - public boolean equals(Object obj) { - if (obj == this) { - return true; + boolean equals(Object obj) { + if (this.is(obj)) { + return true } if (obj instanceof ClassPathResource) { - ClassPathResource otherRes = (ClassPathResource) obj; + ClassPathResource otherRes = (ClassPathResource) obj return (path.equals(otherRes.path) && nullSafeEquals(classLoader, otherRes.classLoader) && - nullSafeEquals(clazz, otherRes.clazz)); + nullSafeEquals(clazz, otherRes.clazz)) } - return false; + return false } private static boolean nullSafeEquals(Object o1, Object o2) { - if (o1 == o2) { - return true; + if (o1.is(o2)) { + return true } if (o1 == null || o2 == null) { - return false; + return false } if (o1.equals(o2)) { - return true; + return true } if (o1.getClass().isArray() && o2.getClass().isArray()) { if (o1 instanceof Object[] && o2 instanceof Object[]) { - return Arrays.equals((Object[]) o1, (Object[]) o2); + return Arrays.equals((Object[]) o1, (Object[]) o2) } if (o1 instanceof boolean[] && o2 instanceof boolean[]) { - return Arrays.equals((boolean[]) o1, (boolean[]) o2); + return Arrays.equals((boolean[]) o1, (boolean[]) o2) } if (o1 instanceof byte[] && o2 instanceof byte[]) { - return Arrays.equals((byte[]) o1, (byte[]) o2); + return Arrays.equals((byte[]) o1, (byte[]) o2) } if (o1 instanceof char[] && o2 instanceof char[]) { - return Arrays.equals((char[]) o1, (char[]) o2); + return Arrays.equals((char[]) o1, (char[]) o2) } if (o1 instanceof double[] && o2 instanceof double[]) { - return Arrays.equals((double[]) o1, (double[]) o2); + return Arrays.equals((double[]) o1, (double[]) o2) } if (o1 instanceof float[] && o2 instanceof float[]) { - return Arrays.equals((float[]) o1, (float[]) o2); + return Arrays.equals((float[]) o1, (float[]) o2) } if (o1 instanceof int[] && o2 instanceof int[]) { - return Arrays.equals((int[]) o1, (int[]) o2); + return Arrays.equals((int[]) o1, (int[]) o2) } if (o1 instanceof long[] && o2 instanceof long[]) { - return Arrays.equals((long[]) o1, (long[]) o2); + return Arrays.equals((long[]) o1, (long[]) o2) } if (o1 instanceof short[] && o2 instanceof short[]) { - return Arrays.equals((short[]) o1, (short[]) o2); + return Arrays.equals((short[]) o1, (short[]) o2) } } - return false; + return false } /** @@ -275,7 +271,8 @@ private static boolean nullSafeEquals(Object o1, Object o2) { * class path location. */ @Override - public int hashCode() { - return path.hashCode(); + int hashCode() { + return path.hashCode() } + } diff --git a/grails-gradle/model/src/main/groovy/org/grails/io/support/DefaultResourceLoader.java b/grails-gradle/model/src/main/groovy/org/grails/io/support/DefaultResourceLoader.groovy similarity index 72% rename from grails-gradle/model/src/main/groovy/org/grails/io/support/DefaultResourceLoader.java rename to grails-gradle/model/src/main/groovy/org/grails/io/support/DefaultResourceLoader.groovy index c3b59f6d521..11e4aa2fb27 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/io/support/DefaultResourceLoader.java +++ b/grails-gradle/model/src/main/groovy/org/grails/io/support/DefaultResourceLoader.groovy @@ -16,10 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.io.support; +package org.grails.io.support -import java.net.MalformedURLException; -import java.net.URL; +import groovy.transform.CompileStatic /** * Default implementation of the ResourceLoader interface. @@ -31,9 +30,10 @@ * @author Juergen Hoeller * @since 10.03.2004 */ -public class DefaultResourceLoader implements ResourceLoader { +@CompileStatic +class DefaultResourceLoader implements ResourceLoader { - private ClassLoader classLoader; + private ClassLoader classLoader /** * Create a new DefaultResourceLoader. @@ -41,23 +41,23 @@ public class DefaultResourceLoader implements ResourceLoader { * at the time of this ResourceLoader's initialization. * @see java.lang.Thread#getContextClassLoader() */ - public DefaultResourceLoader() { - classLoader = getDefaultClassLoader(); + DefaultResourceLoader() { + classLoader = getDefaultClassLoader() } - public static ClassLoader getDefaultClassLoader() { - ClassLoader cl = null; + static ClassLoader getDefaultClassLoader() { + ClassLoader cl = null try { - cl = Thread.currentThread().getContextClassLoader(); + cl = Thread.currentThread().getContextClassLoader() } - catch (Throwable ex) { + catch (Throwable ignored) { // Cannot access thread context ClassLoader - falling back to system class loader... } if (cl == null) { // No thread context class loader -> use class loader of this class. - cl = DefaultResourceLoader.class.getClassLoader(); + cl = DefaultResourceLoader.getClassLoader() } - return cl; + return cl } /** @@ -65,8 +65,8 @@ public static ClassLoader getDefaultClassLoader() { * @param classLoader the ClassLoader to load class path resources with, or null * for using the thread context class loader at the time of actual resource access */ - public DefaultResourceLoader(ClassLoader classLoader) { - this.classLoader = classLoader; + DefaultResourceLoader(ClassLoader classLoader) { + this.classLoader = classLoader } /** @@ -75,30 +75,30 @@ public DefaultResourceLoader(ClassLoader classLoader) { *

The default is that ClassLoader access will happen using the thread context * class loader at the time of this ResourceLoader's initialization. */ - public void setClassLoader(ClassLoader classLoader) { - this.classLoader = classLoader; + void setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader } /** * Return the ClassLoader to load class path resources with. */ - public ClassLoader getClassLoader() { - return classLoader == null ? getDefaultClassLoader() : classLoader; + ClassLoader getClassLoader() { + return classLoader == null ? getDefaultClassLoader() : classLoader } - public Resource getResource(String location) { + Resource getResource(String location) { if (location.startsWith(CLASSPATH_URL_PREFIX)) { - return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()); + return new ClassPathResource(location.substring(CLASSPATH_URL_PREFIX.length()), getClassLoader()) } try { // Try to parse the location as a URL... - URL url = new URL(location); - return new UrlResource(url); + URL url = new URL(location) + return new UrlResource(url) } catch (MalformedURLException ex) { // No URL -> resolve as resource path. - return getResourceByPath(location); + return getResourceByPath(location) } } @@ -112,7 +112,7 @@ public Resource getResource(String location) { * @see ClassPathResource */ protected Resource getResourceByPath(String path) { - return new ClassPathContextResource(path, getClassLoader()); + return new ClassPathContextResource(path, getClassLoader()) } /** @@ -121,19 +121,20 @@ protected Resource getResourceByPath(String path) { */ private static class ClassPathContextResource extends ClassPathResource { - public ClassPathContextResource(String path, ClassLoader classLoader) { - super(path, classLoader); + ClassPathContextResource(String path, ClassLoader classLoader) { + super(path, classLoader) } - @SuppressWarnings("unused") - public String getPathWithinContext() { - return getPath(); + @SuppressWarnings('unused') + String getPathWithinContext() { + return getPath() } @Override - public Resource createRelative(String relativePath) { - String pathToUse = GrailsResourceUtils.applyRelativePath(getPath(), relativePath); - return new ClassPathContextResource(pathToUse, getClassLoader()); + Resource createRelative(String relativePath) { + String pathToUse = GrailsResourceUtils.applyRelativePath(getPath(), relativePath) + return new ClassPathContextResource(pathToUse, getClassLoader()) } } + } diff --git a/grails-gradle/model/src/main/groovy/org/grails/io/support/FileSystemResource.java b/grails-gradle/model/src/main/groovy/org/grails/io/support/FileSystemResource.groovy similarity index 62% rename from grails-gradle/model/src/main/groovy/org/grails/io/support/FileSystemResource.java rename to grails-gradle/model/src/main/groovy/org/grails/io/support/FileSystemResource.groovy index f1243dafc0e..17fe8a141e9 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/io/support/FileSystemResource.java +++ b/grails-gradle/model/src/main/groovy/org/grails/io/support/FileSystemResource.groovy @@ -16,16 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.io.support; +package org.grails.io.support -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.net.URI; -import java.net.URL; +import groovy.transform.CompileStatic /** * Based on Spring FileSystemResource implementation. @@ -36,20 +29,21 @@ * @since 28.12.2003 * @see java.io.File */ -public class FileSystemResource implements Resource { +@CompileStatic +class FileSystemResource implements Resource { - private final File file; - private final String path; + private final File file + private final String path /** * Create a new FileSystemResource from a File handle. * * @param file a File handle */ - public FileSystemResource(File file) { - assertNotNull(file, "File must not be null"); - this.file = file; - path = GrailsResourceUtils.cleanPath(file.getPath()); + FileSystemResource(File file) { + assertNotNull(file, 'File must not be null') + this.file = file + path = GrailsResourceUtils.cleanPath(file.getPath()) } /** @@ -57,25 +51,25 @@ public FileSystemResource(File file) { * * @param path a file path */ - public FileSystemResource(String path) { - assertNotNull(path, "Path must not be null"); - file = new File(path); - this.path = GrailsResourceUtils.cleanPath(path); + FileSystemResource(String path) { + assertNotNull(path, 'Path must not be null') + file = new File(path) + this.path = GrailsResourceUtils.cleanPath(path) } /** * Return the file path for this resource. */ - public final String getPath() { - return path; + final String getPath() { + return path } /** * This implementation returns whether the underlying file exists. * @see java.io.File#exists() */ - public boolean exists() { - return file.exists(); + boolean exists() { + return file.exists() } /** @@ -84,58 +78,58 @@ public boolean exists() { * @see java.io.File#canRead() * @see java.io.File#isDirectory() */ - public boolean isReadable() { - return file.canRead() && !file.isDirectory(); + boolean isReadable() { + return file.canRead() && !file.isDirectory() } /** * This implementation opens a FileInputStream for the underlying file. * @see java.io.FileInputStream */ - public InputStream getInputStream() throws IOException { - return new FileInputStream(file); + InputStream getInputStream() throws IOException { + return new FileInputStream(file) } /** * This implementation returns a URL for the underlying file. * @see java.io.File#toURI() */ - public URL getURL() throws IOException { - return file.toURI().toURL(); + URL getURL() throws IOException { + return file.toURI().toURL() } /** * This implementation returns a URI for the underlying file. * @see java.io.File#toURI() */ - public URI getURI() throws IOException { - return file.toURI(); + URI getURI() throws IOException { + return file.toURI() } /** * This implementation returns the underlying File reference. */ - public File getFile() { - return file; + File getFile() { + return file } /** * This implementation returns the underlying File's length. */ - public long contentLength() throws IOException { - return file.length(); + long contentLength() throws IOException { + return file.length() } - public long lastModified() throws IOException { - return file.lastModified(); + long lastModified() throws IOException { + return file.lastModified() } /** * This implementation returns the name of the file. * @see java.io.File#getName() */ - public String getFilename() { - return file.getName(); + String getFilename() { + return file.getName() } /** @@ -143,17 +137,17 @@ public String getFilename() { * path of the file. * @see java.io.File#getAbsolutePath() */ - public String getDescription() { - return "file [" + file.getAbsolutePath() + "]"; + String getDescription() { + return 'file [' + file.getAbsolutePath() + ']' } /** * This implementation creates a FileSystemResource, applying the given path * relative to the path of the underlying file of this resource descriptor. */ - public Resource createRelative(String relativePath) { - String pathToUse = GrailsResourceUtils.applyRelativePath(path, relativePath); - return new FileSystemResource(pathToUse); + Resource createRelative(String relativePath) { + String pathToUse = GrailsResourceUtils.applyRelativePath(path, relativePath) + return new FileSystemResource(pathToUse) } // implementation of WritableResource @@ -164,43 +158,44 @@ public Resource createRelative(String relativePath) { * @see java.io.File#canWrite() * @see java.io.File#isDirectory() */ - public boolean isWritable() { - return file.canWrite() && !file.isDirectory(); + boolean isWritable() { + return file.canWrite() && !file.isDirectory() } /** * This implementation opens a FileOutputStream for the underlying file. * @see java.io.FileOutputStream */ - public OutputStream getOutputStream() throws IOException { - return new FileOutputStream(file); + OutputStream getOutputStream() throws IOException { + return new FileOutputStream(file) } /** * This implementation compares the underlying File references. */ @Override - public boolean equals(Object obj) { - return (obj == this || - (obj instanceof FileSystemResource && path.equals(((FileSystemResource) obj).path))); + boolean equals(Object obj) { + return (this.is(obj) || + (obj instanceof FileSystemResource && path.equals(((FileSystemResource) obj).path))) } /** * This implementation returns the hash code of the underlying File reference. */ @Override - public int hashCode() { - return path.hashCode(); + int hashCode() { + return path.hashCode() } protected void assertNotNull(Object object, String message) { if (object == null) { - throw new IllegalArgumentException(message); + throw new IllegalArgumentException(message) } } @Override - public String toString() { - return file.toString(); + String toString() { + return file.toString() } + } diff --git a/grails-gradle/model/src/main/groovy/org/grails/io/support/GrailsResourceUtils.java b/grails-gradle/model/src/main/groovy/org/grails/io/support/GrailsResourceUtils.groovy similarity index 60% rename from grails-gradle/model/src/main/groovy/org/grails/io/support/GrailsResourceUtils.java rename to grails-gradle/model/src/main/groovy/org/grails/io/support/GrailsResourceUtils.groovy index e0141a999a6..545c10a5060 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/io/support/GrailsResourceUtils.java +++ b/grails-gradle/model/src/main/groovy/org/grails/io/support/GrailsResourceUtils.groovy @@ -16,31 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.io.support; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URISyntaxException; -import java.net.URL; -import java.net.URLConnection; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import groovy.lang.Closure; -import groovy.util.ConfigObject; -import org.codehaus.groovy.runtime.DefaultGroovyMethods; - -import grails.util.BuildSettings; +package org.grails.io.support + +import java.util.regex.Matcher +import java.util.regex.Pattern + +import groovy.transform.CompileStatic +import org.codehaus.groovy.runtime.DefaultGroovyMethods + +import grails.util.BuildSettings /** * Utility methods for resource handling / figuring out class names. @@ -49,171 +33,181 @@ * @author Juergen Hoeller * @since 2.0 */ -public class GrailsResourceUtils { +@CompileStatic +class GrailsResourceUtils { - public static final String CLASS_EXTENSION = ".class"; + public static final String CLASS_EXTENSION = '.class' - private static final String WINDOWS_FOLDER_SEPARATOR = "\\"; + private static final String WINDOWS_FOLDER_SEPARATOR = '\\' - private static final String TOP_PATH = ".."; + private static final String TOP_PATH = '..' - private static final String CURRENT_PATH = "."; + private static final String CURRENT_PATH = '.' - private static final String FOLDER_SEPARATOR = "/"; - public static final String JAR_URL_SEPARATOR = "!/"; + private static final String FOLDER_SEPARATOR = '/' + public static final String JAR_URL_SEPARATOR = '!/' /** Pseudo URL prefix for loading from the class path: "classpath:" */ - public static final String CLASSPATH_URL_PREFIX = "classpath:"; + public static final String CLASSPATH_URL_PREFIX = 'classpath:' /** URL prefix for loading from the file system: "file:" */ - public static final String FILE_URL_PREFIX = "file:"; + public static final String FILE_URL_PREFIX = 'file:' /** URL protocol for a file in the file system: "file" */ - public static final String URL_PROTOCOL_FILE = "file"; + public static final String URL_PROTOCOL_FILE = 'file' /** URL protocol for an entry from a jar file: "jar" */ - public static final String URL_PROTOCOL_JAR = "jar"; + public static final String URL_PROTOCOL_JAR = 'jar' /** URL protocol for an entry from a zip file: "zip" */ - public static final String URL_PROTOCOL_ZIP = "zip"; + public static final String URL_PROTOCOL_ZIP = 'zip' /** URL protocol for an entry from a JBoss jar file: "vfszip" */ - public static final String URL_PROTOCOL_VFSZIP = "vfszip"; + public static final String URL_PROTOCOL_VFSZIP = 'vfszip' /** URL protocol for a JBoss VFS resource: "vfs" */ - public static final String URL_PROTOCOL_VFS = "vfs"; + public static final String URL_PROTOCOL_VFS = 'vfs' /** URL protocol for an entry from a WebSphere jar file: "wsjar" */ - public static final String URL_PROTOCOL_WSJAR = "wsjar"; + public static final String URL_PROTOCOL_WSJAR = 'wsjar' /** URL protocol for an entry from an OC4J jar file: "code-source" */ - public static final String URL_PROTOCOL_CODE_SOURCE = "code-source"; + public static final String URL_PROTOCOL_CODE_SOURCE = 'code-source' /** * The relative path to the WEB-INF directory */ - public static final String WEB_INF = "/WEB-INF"; + public static final String WEB_INF = '/WEB-INF' /** * The name of the Grails application directory */ - public static final String GRAILS_APP_DIR = "grails-app"; + public static final String GRAILS_APP_DIR = 'grails-app' /** * The name of the Web app dir within Grails */ - public static final String WEB_APP_DIR = "web-app"; + public static final String WEB_APP_DIR = 'web-app' /** * The path to the views directory */ - public static final String VIEWS_DIR_PATH = GRAILS_APP_DIR + "/views/"; + public static final String VIEWS_DIR_PATH = GRAILS_APP_DIR + '/views/' /** * The path to the views directory without a trailing slash */ - public static final String VIEWS_DIR_PATH_NO_SLASH = GRAILS_APP_DIR + "/views"; + public static final String VIEWS_DIR_PATH_NO_SLASH = GRAILS_APP_DIR + '/views' - public static final String DOMAIN_DIR_PATH = GRAILS_APP_DIR + "/domain/"; + public static final String DOMAIN_DIR_PATH = GRAILS_APP_DIR + '/domain/' - public static final String REGEX_FILE_SEPARATOR = "[\\\\/]"; // backslashes need escaping in regexes + public static final String REGEX_FILE_SEPARATOR = '[\\\\/]' // backslashes need escaping in regexes /* Domain path is always matched against the normalized File representation of an URL and can therefore work with slashes as separators. */ - public static Pattern DOMAIN_PATH_PATTERN = Pattern.compile(".+" + REGEX_FILE_SEPARATOR + GRAILS_APP_DIR + REGEX_FILE_SEPARATOR + "domain" + REGEX_FILE_SEPARATOR + "(.+)\\.(groovy|java)"); + static Pattern DOMAIN_PATH_PATTERN = Pattern.compile('.+' + REGEX_FILE_SEPARATOR + GRAILS_APP_DIR + REGEX_FILE_SEPARATOR + 'domain' + REGEX_FILE_SEPARATOR + '(.+)\\.(groovy|java)') /* This pattern will match any resource within a given directory inside grails-app */ - public static Pattern RESOURCE_PATH_PATTERN = Pattern.compile(".+?" + REGEX_FILE_SEPARATOR + GRAILS_APP_DIR + REGEX_FILE_SEPARATOR + "(.+?)" + REGEX_FILE_SEPARATOR + "(.+?\\.(groovy|java))"); + static Pattern RESOURCE_PATH_PATTERN = Pattern.compile('.+?' + REGEX_FILE_SEPARATOR + GRAILS_APP_DIR + REGEX_FILE_SEPARATOR + '(.+?)' + REGEX_FILE_SEPARATOR + '(.+?\\.(groovy|java))') - public static Pattern SPRING_SCRIPTS_PATH_PATTERN = Pattern.compile(".+?" + REGEX_FILE_SEPARATOR + GRAILS_APP_DIR + REGEX_FILE_SEPARATOR + "conf" + REGEX_FILE_SEPARATOR + "spring" + REGEX_FILE_SEPARATOR + "(.+?\\.groovy)"); + static Pattern SPRING_SCRIPTS_PATH_PATTERN = Pattern.compile('.+?' + REGEX_FILE_SEPARATOR + GRAILS_APP_DIR + REGEX_FILE_SEPARATOR + 'conf' + REGEX_FILE_SEPARATOR + 'spring' + REGEX_FILE_SEPARATOR + '(.+?\\.groovy)') - public static Pattern[] COMPILER_ROOT_PATTERNS = { + static Pattern[] COMPILER_ROOT_PATTERNS = [ SPRING_SCRIPTS_PATH_PATTERN, RESOURCE_PATH_PATTERN - }; + ] as Pattern[] /* Resources are resolved against the platform specific path and must therefore obey the specific File.separator. */ - public static final Pattern GRAILS_RESOURCE_PATTERN_FIRST_MATCH; - public static final Pattern GRAILS_RESOURCE_PATTERN_SECOND_MATCH; - public static final Pattern GRAILS_RESOURCE_PATTERN_THIRD_MATCH; - public static final Pattern GRAILS_RESOURCE_PATTERN_FOURTH_MATCH; - public static final Pattern GRAILS_RESOURCE_PATTERN_FIFTH_MATCH; - public static final Pattern GRAILS_RESOURCE_PATTERN_SIXTH_MATCH; - public static final Pattern GRAILS_RESOURCE_PATTERN_SEVENTH_MATCH; - public static final Pattern GRAILS_RESOURCE_PATTERN_EIGHTH_MATCH; - public static final Pattern GRAILS_RESOURCE_PATTERN_NINTH_MATCH; - public static final Pattern GRAILS_RESOURCE_PATTERN_TENTH_MATCH; - public static final Pattern GRAILS_RESOURCE_PATTERN_ELEVENTH_MATCH; + public static final Pattern GRAILS_RESOURCE_PATTERN_FIRST_MATCH + public static final Pattern GRAILS_RESOURCE_PATTERN_SECOND_MATCH + public static final Pattern GRAILS_RESOURCE_PATTERN_THIRD_MATCH + public static final Pattern GRAILS_RESOURCE_PATTERN_FOURTH_MATCH + public static final Pattern GRAILS_RESOURCE_PATTERN_FIFTH_MATCH + public static final Pattern GRAILS_RESOURCE_PATTERN_SIXTH_MATCH + public static final Pattern GRAILS_RESOURCE_PATTERN_SEVENTH_MATCH + public static final Pattern GRAILS_RESOURCE_PATTERN_EIGHTH_MATCH + public static final Pattern GRAILS_RESOURCE_PATTERN_NINTH_MATCH + public static final Pattern GRAILS_RESOURCE_PATTERN_TENTH_MATCH + public static final Pattern GRAILS_RESOURCE_PATTERN_ELEVENTH_MATCH static { - String fs = REGEX_FILE_SEPARATOR; - - GRAILS_RESOURCE_PATTERN_FIRST_MATCH = Pattern.compile(createGrailsResourcePattern(fs, GRAILS_APP_DIR + fs + "conf" + fs + "spring")); - GRAILS_RESOURCE_PATTERN_THIRD_MATCH = Pattern.compile(createGrailsResourcePattern(fs, GRAILS_APP_DIR + fs + "[\\w-]+")); - GRAILS_RESOURCE_PATTERN_SEVENTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, "src" + fs + "main" + fs + "java")); - GRAILS_RESOURCE_PATTERN_EIGHTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, "src" + fs + "main" + fs + "groovy")); - - GRAILS_RESOURCE_PATTERN_NINTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, "src" + fs + "test" + fs + "groovy")); - GRAILS_RESOURCE_PATTERN_TENTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, "src" + fs + "test" + fs + "java")); - GRAILS_RESOURCE_PATTERN_ELEVENTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, "src" + fs + "test" + fs + "functional")); - - GRAILS_RESOURCE_PATTERN_FIFTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, "grails-tests")); - fs = "/"; - GRAILS_RESOURCE_PATTERN_SECOND_MATCH = Pattern.compile(createGrailsResourcePattern(fs, GRAILS_APP_DIR + fs + "conf" + fs + "spring")); - GRAILS_RESOURCE_PATTERN_FOURTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, GRAILS_APP_DIR + fs + "[\\w-]+")); - GRAILS_RESOURCE_PATTERN_SIXTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, "grails-tests")); - } - - public static final Pattern[] patterns = new Pattern[]{ - GRAILS_RESOURCE_PATTERN_FIRST_MATCH, - GRAILS_RESOURCE_PATTERN_THIRD_MATCH, - GRAILS_RESOURCE_PATTERN_SEVENTH_MATCH, - GRAILS_RESOURCE_PATTERN_EIGHTH_MATCH, - GRAILS_RESOURCE_PATTERN_FOURTH_MATCH, - GRAILS_RESOURCE_PATTERN_FIFTH_MATCH, - GRAILS_RESOURCE_PATTERN_SIXTH_MATCH, - GRAILS_RESOURCE_PATTERN_NINTH_MATCH, - GRAILS_RESOURCE_PATTERN_TENTH_MATCH, - GRAILS_RESOURCE_PATTERN_ELEVENTH_MATCH - }; - - public static final Pattern[] grailsAppResourcePatterns = new Pattern[]{ - GRAILS_RESOURCE_PATTERN_FIRST_MATCH, - GRAILS_RESOURCE_PATTERN_THIRD_MATCH, - GRAILS_RESOURCE_PATTERN_FOURTH_MATCH, - GRAILS_RESOURCE_PATTERN_FIFTH_MATCH, - GRAILS_RESOURCE_PATTERN_SIXTH_MATCH, - GRAILS_RESOURCE_PATTERN_ELEVENTH_MATCH - }; - - private static Map KNOWN_PATHS = new LinkedHashMap<>() { + String fs = REGEX_FILE_SEPARATOR + + GRAILS_RESOURCE_PATTERN_FIRST_MATCH = Pattern.compile(createGrailsResourcePattern(fs, GRAILS_APP_DIR + fs + 'conf' + fs + 'spring')) + GRAILS_RESOURCE_PATTERN_THIRD_MATCH = Pattern.compile(createGrailsResourcePattern(fs, GRAILS_APP_DIR + fs + '[\\w-]+')) + GRAILS_RESOURCE_PATTERN_SEVENTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, 'src' + fs + 'main' + fs + 'java')) + GRAILS_RESOURCE_PATTERN_EIGHTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, 'src' + fs + 'main' + fs + 'groovy')) + + GRAILS_RESOURCE_PATTERN_NINTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, 'src' + fs + 'test' + fs + 'groovy')) + GRAILS_RESOURCE_PATTERN_TENTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, 'src' + fs + 'test' + fs + 'java')) + GRAILS_RESOURCE_PATTERN_ELEVENTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, 'src' + fs + 'test' + fs + 'functional')) + + GRAILS_RESOURCE_PATTERN_FIFTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, 'grails-tests')) + fs = '/' + GRAILS_RESOURCE_PATTERN_SECOND_MATCH = Pattern.compile(createGrailsResourcePattern(fs, GRAILS_APP_DIR + fs + 'conf' + fs + 'spring')) + GRAILS_RESOURCE_PATTERN_FOURTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, GRAILS_APP_DIR + fs + '[\\w-]+')) + GRAILS_RESOURCE_PATTERN_SIXTH_MATCH = Pattern.compile(createGrailsResourcePattern(fs, 'grails-tests')) + + // Assigned here, not as separate field initializers below: Groovy does not preserve + // Java's textual static-initialization order across a static {} block and later field + // initializers, so referencing these fields from a field initializer placed after this + // block would silently capture null (confirmed empirically) rather than the values just + // assigned above. + patterns = new Pattern[]{ + GRAILS_RESOURCE_PATTERN_FIRST_MATCH, + GRAILS_RESOURCE_PATTERN_THIRD_MATCH, + GRAILS_RESOURCE_PATTERN_SEVENTH_MATCH, + GRAILS_RESOURCE_PATTERN_EIGHTH_MATCH, + GRAILS_RESOURCE_PATTERN_FOURTH_MATCH, + GRAILS_RESOURCE_PATTERN_FIFTH_MATCH, + GRAILS_RESOURCE_PATTERN_SIXTH_MATCH, + GRAILS_RESOURCE_PATTERN_NINTH_MATCH, + GRAILS_RESOURCE_PATTERN_TENTH_MATCH, + GRAILS_RESOURCE_PATTERN_ELEVENTH_MATCH + } + + grailsAppResourcePatterns = new Pattern[]{ + GRAILS_RESOURCE_PATTERN_FIRST_MATCH, + GRAILS_RESOURCE_PATTERN_THIRD_MATCH, + GRAILS_RESOURCE_PATTERN_FOURTH_MATCH, + GRAILS_RESOURCE_PATTERN_FIFTH_MATCH, + GRAILS_RESOURCE_PATTERN_SIXTH_MATCH, + GRAILS_RESOURCE_PATTERN_ELEVENTH_MATCH + } + } + + public static final Pattern[] patterns + + public static final Pattern[] grailsAppResourcePatterns + + private static Map KNOWN_PATHS = new LinkedHashMap() { @Override protected boolean removeEldestEntry(Map.Entry eldest) { - return this.size() > 100; + return this.size() > 100 } - }; + } private static Map KNOWN_DOMAIN_CLASSES = DefaultGroovyMethods.withDefault(new LinkedHashMap() { @Override protected boolean removeEldestEntry(Map.Entry eldest) { - return this.size() > 100; + return this.size() > 100 } - }, new Closure(GrailsResourceUtils.class) { + }, new Closure(GrailsResourceUtils) { @Override - public Object call(Object... args) { - String path = args[0].toString(); - return DOMAIN_PATH_PATTERN.matcher(path).find(); + Object call(Object... args) { + String path = args[0].toString() + return DOMAIN_PATH_PATTERN.matcher(path).find() } - }); + }) private static String createGrailsResourcePattern(String separator, String base) { - return ".+" + separator + base + separator + "(.+)\\.(groovy|java)$"; + return '.+' + separator + base + separator + '(.+)\\.(groovy|java)\$' } /** @@ -223,9 +217,9 @@ private static String createGrailsResourcePattern(String separator, String base) * @return true if it is a domain class */ - public static boolean isDomainClass(URL url) { - if (url == null) return false; - return KNOWN_DOMAIN_CLASSES.get(url.getFile()); + static boolean isDomainClass(URL url) { + if (url == null) return false + return KNOWN_DOMAIN_CLASSES.get(url.getFile()) } /** @@ -234,12 +228,12 @@ public static boolean isDomainClass(URL url) { * @param path the file path (may be null) * @return the extracted filename, or null if none */ - public static String getFilename(String path) { + static String getFilename(String path) { if (path == null) { - return null; + return null } - int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); - return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path); + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR) + return (separatorIndex != -1 ? path.substring(separatorIndex + 1) : path) } /** @@ -256,21 +250,21 @@ public static String getFilename(String path) { * @see ClassLoader#getResource * @see Class#getResource */ - public static String classPackageAsResourcePath(Class clazz) { + static String classPackageAsResourcePath(Class clazz) { if (clazz == null) { - return ""; + return '' } - String className = clazz.getName(); - int packageEndIndex = className.lastIndexOf('.'); + String className = clazz.getName() + int packageEndIndex = className.lastIndexOf('.') if (packageEndIndex == -1) { - return ""; + return '' } - String packageName = className.substring(0, packageEndIndex); - return packageName.replace('.', '/'); + String packageName = className.substring(0, packageEndIndex) + return packageName.replace('.', '/') } - public static void useCachesIfNecessary(URLConnection con) { - con.setUseCaches(con.getClass().getName().startsWith("JNLP")); + static void useCachesIfNecessary(URLConnection con) { + con.setUseCaches(con.getClass().getName().startsWith('JNLP')) } /** @@ -279,12 +273,12 @@ public static void useCachesIfNecessary(URLConnection con) { * @param resource The Spring Resource * @return The class name or null if the resource is not a Grails class */ - public static String getClassName(Resource resource) { + static String getClassName(Resource resource) { try { - return getClassName(resource.getFile().getAbsolutePath()); + return getClassName(resource.getFile().getAbsolutePath()) } catch (IOException e) { - return null; + return null } } @@ -294,14 +288,14 @@ public static String getClassName(Resource resource) { * @param path The path to check * @return The class name or null if it doesn't exist */ - public static String getClassName(String path) { - for (Pattern pattern : patterns) { - Matcher m = pattern.matcher(path); + static String getClassName(String path) { + for (Pattern pattern in patterns) { + Matcher m = pattern.matcher(path) if (m.find()) { - return m.group(1).replaceAll("[/\\\\]", "."); + return m.group(1).replaceAll('[/\\\\]', '.') } } - return null; + return null } /** @@ -310,14 +304,14 @@ public static String getClassName(String path) { * @param path The path to check * @return The class name or null if it doesn't exist */ - public static String getClassNameForClassFile(String rootDir, String path) { - path = path.replace("/", "."); - path = path.replace('\\', '.'); - path = path.substring(0, path.length() - CLASS_EXTENSION.length()); + static String getClassNameForClassFile(String rootDir, String path) { + path = path.replace('/', '.') + path = path.replace('\\', '.') + path = path.substring(0, path.length() - CLASS_EXTENSION.length()) if (rootDir != null) { - path = path.substring(rootDir.length()); + path = path.substring(rootDir.length()) } - return path; + return path } /** @@ -330,18 +324,18 @@ public static String getClassNameForClassFile(String rootDir, String path) { * @throws java.io.FileNotFoundException if the URL cannot be resolved to * a file in the file system */ - public static File getFile(URL resourceUrl, String description) throws FileNotFoundException { + static File getFile(URL resourceUrl, String description) throws FileNotFoundException { if (!URL_PROTOCOL_FILE.equals(resourceUrl.getProtocol())) { throw new FileNotFoundException( - description + " cannot be resolved to absolute file path " + - "because it does not reside in the file system: " + resourceUrl); + description + ' cannot be resolved to absolute file path ' + + 'because it does not reside in the file system: ' + resourceUrl) } try { - return new File(toURI(resourceUrl).getSchemeSpecificPart()); + return new File(toURI(resourceUrl).getSchemeSpecificPart()) } catch (URISyntaxException ex) { // Fallback for URLs that are not valid URIs (should hardly ever happen). - return new File(resourceUrl.getFile()); + return new File(resourceUrl.getFile()) } } @@ -354,12 +348,12 @@ public static File getFile(URL resourceUrl, String description) throws FileNotFo * @param url the URL to check * @return whether the URL has been identified as a JAR URL */ - public static boolean isJarURL(URL url) { - String protocol = url.getProtocol(); + static boolean isJarURL(URL url) { + String protocol = url.getProtocol() return (URL_PROTOCOL_JAR.equals(protocol) || URL_PROTOCOL_ZIP.equals(protocol) || URL_PROTOCOL_WSJAR.equals(protocol) || - (URL_PROTOCOL_CODE_SOURCE.equals(protocol) && url.getPath().contains(JAR_URL_SEPARATOR))); + (URL_PROTOCOL_CODE_SOURCE.equals(protocol) && url.getPath().contains(JAR_URL_SEPARATOR))) } /** @@ -372,13 +366,13 @@ public static boolean isJarURL(URL url) { * @throws FileNotFoundException if the URL cannot be resolved to * a file in the file system */ - public static File getFile(URI resourceUri, String description) throws FileNotFoundException { + static File getFile(URI resourceUri, String description) throws FileNotFoundException { if (!URL_PROTOCOL_FILE.equals(resourceUri.getScheme())) { throw new FileNotFoundException( - description + " cannot be resolved to absolute file path " + - "because it does not reside in the file system: " + resourceUri); + description + ' cannot be resolved to absolute file path ' + + 'because it does not reside in the file system: ' + resourceUri) } - return new File(resourceUri.getSchemeSpecificPart()); + return new File(resourceUri.getSchemeSpecificPart()) } /** @@ -389,8 +383,8 @@ public static File getFile(URI resourceUri, String description) throws FileNotFo * @throws FileNotFoundException if the URL cannot be resolved to * a file in the file system */ - public static File getFile(URI resourceUri) throws FileNotFoundException { - return getFile(resourceUri, "URI"); + static File getFile(URI resourceUri) throws FileNotFoundException { + return getFile(resourceUri, 'URI') } /** @@ -403,8 +397,8 @@ public static File getFile(URI resourceUri) throws FileNotFoundException { * @throws URISyntaxException if the URL wasn't a valid URI * @see java.net.URL#toURI() */ - public static URI toURI(URL url) throws URISyntaxException { - return toURI(url.toString()); + static URI toURI(URL url) throws URISyntaxException { + return toURI(url.toString()) } /** @@ -413,9 +407,9 @@ public static URI toURI(URL url) throws URISyntaxException { * @param url the URL to check * @return whether the URL has been identified as a file system URL */ - public static boolean isFileURL(URL url) { - String protocol = url.getProtocol(); - return (URL_PROTOCOL_FILE.equals(protocol) || protocol.startsWith(URL_PROTOCOL_VFS)); + static boolean isFileURL(URL url) { + String protocol = url.getProtocol() + return (URL_PROTOCOL_FILE.equals(protocol) || protocol.startsWith(URL_PROTOCOL_VFS)) } /** @@ -426,16 +420,16 @@ public static boolean isFileURL(URL url) { * (relative to the full file path above) * @return the full file path that results from applying the relative path */ - public static String applyRelativePath(String path, String relativePath) { - int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR); + static String applyRelativePath(String path, String relativePath) { + int separatorIndex = path.lastIndexOf(FOLDER_SEPARATOR) if (separatorIndex != -1) { - String newPath = path.substring(0, separatorIndex); + String newPath = path.substring(0, separatorIndex) if (!relativePath.startsWith(FOLDER_SEPARATOR)) { - newPath += FOLDER_SEPARATOR; + newPath += FOLDER_SEPARATOR } - return newPath + relativePath; + return newPath + relativePath } - return relativePath; + return relativePath } /** @@ -446,77 +440,77 @@ public static String applyRelativePath(String path, String relativePath) { * @param path the original path * @return the normalized path */ - public static String cleanPath(String path) { + static String cleanPath(String path) { if (path == null) { - return null; + return null } - String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR); + String pathToUse = replace(path, WINDOWS_FOLDER_SEPARATOR, FOLDER_SEPARATOR) // Strip prefix from path to analyze, to not treat it as part of the // first path element. This is necessary to correctly parse paths like // "file:core/../core/io/Resource.class", where the ".." should just // strip the first "core" directory while keeping the "file:" prefix. - int prefixIndex = pathToUse.indexOf(":"); - String prefix = ""; + int prefixIndex = pathToUse.indexOf(':') + String prefix = '' if (prefixIndex != -1) { - prefix = pathToUse.substring(0, prefixIndex + 1); - pathToUse = pathToUse.substring(prefixIndex + 1); + prefix = pathToUse.substring(0, prefixIndex + 1) + pathToUse = pathToUse.substring(prefixIndex + 1) } if (pathToUse.startsWith(FOLDER_SEPARATOR)) { - prefix = prefix + FOLDER_SEPARATOR; - pathToUse = pathToUse.substring(1); + prefix = prefix + FOLDER_SEPARATOR + pathToUse = pathToUse.substring(1) } - String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR); - List pathElements = new LinkedList<>(); - int tops = 0; + String[] pathArray = delimitedListToStringArray(pathToUse, FOLDER_SEPARATOR) + List pathElements = new LinkedList<>() + int tops = 0 for (int i = pathArray.length - 1; i >= 0; i--) { - String element = pathArray[i]; + String element = pathArray[i] if (CURRENT_PATH.equals(element)) { // Points to current directory - drop it. } else if (TOP_PATH.equals(element)) { // Registering top path found. - tops++; + tops++ } else { if (tops > 0) { // Merging path element with element corresponding to top path. - tops--; + tops-- } else { // Normal path element found. - pathElements.add(0, element); + pathElements.add(0, element) } } } // Remaining top paths need to be retained. for (int i = 0; i < tops; i++) { - pathElements.add(0, TOP_PATH); + pathElements.add(0, TOP_PATH) } - return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR); + return prefix + collectionToDelimitedString(pathElements, FOLDER_SEPARATOR) } private static String collectionToDelimitedString(Collection coll, String delim) { - return collectionToDelimitedString(coll, delim, "", ""); + return collectionToDelimitedString(coll, delim, '', '') } private static String collectionToDelimitedString(Collection coll, String delim, String prefix, String suffix) { if (coll == null || coll.isEmpty()) { - return ""; + return '' } - StringBuilder sb = new StringBuilder(); - Iterator it = coll.iterator(); + StringBuilder sb = new StringBuilder() + Iterator it = coll.iterator() while (it.hasNext()) { - sb.append(prefix).append(it.next()).append(suffix); + sb.append(prefix).append(it.next()).append(suffix) if (it.hasNext()) { - sb.append(delim); + sb.append(delim) } } - return sb.toString(); + return sb.toString() } /** @@ -530,7 +524,7 @@ private static String collectionToDelimitedString(Collection coll, String del * @return an array of the tokens in the list */ private static String[] delimitedListToStringArray(String str, String delimiter) { - return delimitedListToStringArray(str, delimiter, null); + return delimitedListToStringArray(str, delimiter, null) } /** @@ -547,51 +541,51 @@ private static String[] delimitedListToStringArray(String str, String delimiter) */ private static String[] delimitedListToStringArray(String str, String delimiter, String charsToDelete) { if (str == null) { - return new String[0]; + return new String[0] } if (delimiter == null) { - return new String[] {str}; + return [str] as String[] } - List result = new ArrayList<>(); - if ("".equals(delimiter)) { + List result = new ArrayList<>() + if (''.equals(delimiter)) { for (int i = 0; i < str.length(); i++) { - result.add(deleteAny(str.substring(i, i + 1), charsToDelete)); + result.add(deleteAny(str.substring(i, i + 1), charsToDelete)) } } else { - int pos = 0; - int delPos; + int pos = 0 + int delPos while ((delPos = str.indexOf(delimiter, pos)) != -1) { - result.add(deleteAny(str.substring(pos, delPos), charsToDelete)); - pos = delPos + delimiter.length(); + result.add(deleteAny(str.substring(pos, delPos), charsToDelete)) + pos = delPos + delimiter.length() } if (str.length() > 0 && pos <= str.length()) { // Add rest of String, but not in case of empty input. - result.add(deleteAny(str.substring(pos), charsToDelete)); + result.add(deleteAny(str.substring(pos), charsToDelete)) } } - return toStringArray(result); + return toStringArray(result) } private static String[] toStringArray(Collection collection) { if (collection == null) { - return null; + return null } - return collection.toArray(new String[collection.size()]); + return collection.toArray(new String[collection.size()]) } private static String deleteAny(String inString, String charsToDelete) { if (!hasLength(inString) || !hasLength(charsToDelete)) { - return inString; + return inString } - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new StringBuilder() for (int i = 0; i < inString.length(); i++) { - char c = inString.charAt(i); - if (charsToDelete.indexOf(c) == -1) { - sb.append(c); + char c = inString.charAt(i) + if (charsToDelete.indexOf((int) c) == -1) { + sb.append(c) } } - return sb.toString(); + return sb.toString() } /** @@ -604,26 +598,26 @@ private static String deleteAny(String inString, String charsToDelete) { */ private static String replace(String inString, String oldPattern, String newPattern) { if (!hasLength(inString) || !hasLength(oldPattern) || newPattern == null) { - return inString; + return inString } - StringBuilder sb = new StringBuilder(); - int pos = 0; // our position in the old string - int index = inString.indexOf(oldPattern); + StringBuilder sb = new StringBuilder() + int pos = 0 // our position in the old string + int index = inString.indexOf(oldPattern) // the index of an occurrence we've found, or -1 - int patLen = oldPattern.length(); + int patLen = oldPattern.length() while (index >= 0) { - sb.append(inString.substring(pos, index)); - sb.append(newPattern); - pos = index + patLen; - index = inString.indexOf(oldPattern, pos); + sb.append(inString.substring(pos, index)) + sb.append(newPattern) + pos = index + patLen + index = inString.indexOf(oldPattern, pos) } - sb.append(inString.substring(pos)); + sb.append(inString.substring(pos)) // remember to append any characters to the right of a match - return sb.toString(); + return sb.toString() } private static boolean hasLength(CharSequence str) { - return (str != null && str.length() > 0); + return (str != null && str.length() > 0) } /** @@ -633,24 +627,24 @@ private static boolean hasLength(CharSequence str) { * @return the URL for the actual jar file * @throws MalformedURLException if no valid jar file URL could be extracted */ - public static URL extractJarFileURL(URL jarUrl) throws MalformedURLException { - String urlFile = jarUrl.getFile(); - int separatorIndex = urlFile.indexOf(JAR_URL_SEPARATOR); + static URL extractJarFileURL(URL jarUrl) throws MalformedURLException { + String urlFile = jarUrl.getFile() + int separatorIndex = urlFile.indexOf(JAR_URL_SEPARATOR) if (separatorIndex != -1) { - String jarFile = urlFile.substring(0, separatorIndex); + String jarFile = urlFile.substring(0, separatorIndex) try { - return new URL(jarFile); + return new URL(jarFile) } catch (MalformedURLException ex) { // Probably no protocol in original jar URL, like "jar:C:/mypath/myjar.jar". // This usually indicates that the jar file resides in the file system. - if (!jarFile.startsWith("/")) { - jarFile = "/" + jarFile; + if (!jarFile.startsWith('/')) { + jarFile = '/' + jarFile } - return new URL(FILE_URL_PREFIX + jarFile); + return new URL(FILE_URL_PREFIX + jarFile) } } - return jarUrl; + return jarUrl } /** @@ -660,8 +654,8 @@ public static URL extractJarFileURL(URL jarUrl) throws MalformedURLException { * @return the URI instance * @throws URISyntaxException if the location wasn't a valid URI */ - public static URI toURI(String location) throws URISyntaxException { - return new URI(replace(location, " ", "%20")); + static URI toURI(String location) throws URISyntaxException { + return new URI(replace(location, ' ', '%20')) } /** @@ -671,19 +665,19 @@ public static URI toURI(String location) throws URISyntaxException { * @return true if it is a Grails path */ - public static boolean isGrailsPath(String path) { + static boolean isGrailsPath(String path) { if (KNOWN_PATHS.containsKey(path)) { - return KNOWN_PATHS.get(path); + return KNOWN_PATHS.get(path) } - for (Pattern grailsAppResourcePattern : grailsAppResourcePatterns) { - Matcher m = grailsAppResourcePattern.matcher(path); + for (Pattern grailsAppResourcePattern in grailsAppResourcePatterns) { + Matcher m = grailsAppResourcePattern.matcher(path) if (m.find()) { - KNOWN_PATHS.put(path, true); - return true; + KNOWN_PATHS.put(path, true) + return true } } - KNOWN_PATHS.put(path, false); - return false; + KNOWN_PATHS.put(path, false) + return false } /** @@ -692,14 +686,14 @@ public static boolean isGrailsPath(String path) { * @param path The path to check * @return true if it is a Grails path */ - public static boolean isProjectSource(String path) { - for (Pattern grailsAppResourcePattern : patterns) { - Matcher m = grailsAppResourcePattern.matcher(path); + static boolean isProjectSource(String path) { + for (Pattern grailsAppResourcePattern in patterns) { + Matcher m = grailsAppResourcePattern.matcher(path) if (m.find()) { - return true; + return true } } - return false; + return false } /** @@ -708,13 +702,13 @@ public static boolean isProjectSource(String path) { * @param r The resoruce to check * @return true if it is a Grails path */ - public static boolean isProjectSource(Resource r) { + static boolean isProjectSource(Resource r) { try { - String file = r.getURL().getFile(); - return isProjectSource(file) || file.endsWith("GrailsPlugin.groovy"); + String file = r.getURL().getFile() + return isProjectSource(file) || file.endsWith('GrailsPlugin.groovy') } catch (IOException e) { - return false; + return false } } @@ -724,58 +718,58 @@ public static boolean isProjectSource(Resource r) { * @param r The resource to check * @return True if it is a Grails resource */ - public static boolean isGrailsResource(Resource r) { + static boolean isGrailsResource(Resource r) { try { - String file = r.getURL().getFile(); - return isGrailsPath(file) || file.endsWith("GrailsPlugin.groovy"); + String file = r.getURL().getFile() + return isGrailsPath(file) || file.endsWith('GrailsPlugin.groovy') } catch (IOException e) { - return false; + return false } } - public static Resource getViewsDir(Resource resource) { - if (resource == null) return null; + static Resource getViewsDir(Resource resource) { + if (resource == null) return null - Resource appDir = getAppDir(resource); - if (appDir == null) return null; - return appDir.createRelative("views"); + Resource appDir = getAppDir(resource) + if (appDir == null) return null + return appDir.createRelative('views') } - public static Resource getAppDir(Resource resource) { - if (resource == null) return null; + static Resource getAppDir(Resource resource) { + if (resource == null) return null try { - File file = resource.getFile(); + File file = resource.getFile() while (file != null && !file.getName().equals(GRAILS_APP_DIR)) { - file = file.getParentFile(); + file = file.getParentFile() } if (file != null) { - return new FileSystemResource(file.getAbsolutePath() + '/'); + return new FileSystemResource(file.getAbsolutePath() + '/') } - } catch (IOException e) { + } catch (IOException ignored) { } try { - String url = resource.getURL().toString(); + String url = resource.getURL().toString() - int i = url.lastIndexOf(GRAILS_APP_DIR); + int i = url.lastIndexOf(GRAILS_APP_DIR) if (i > -1) { - url = url.substring(0, i + 10); - return new UrlResource(url + '/'); + url = url.substring(0, i + 10) + return new UrlResource(url + '/') } - return null; + return null } catch (MalformedURLException e) { - return null; + return null } catch (IOException e) { - return null; + return null } } - private static final Pattern PLUGIN_PATTERN = Pattern.compile(".+?(/plugins/.+?/" + GRAILS_APP_DIR + "/.+)"); + private static final Pattern PLUGIN_PATTERN = Pattern.compile('.+?(/plugins/.+?/' + GRAILS_APP_DIR + '/.+)') /** * Takes a Grails resource (one located inside the grails-app dir) and gets its relative path inside the WEB-INF directory @@ -784,33 +778,33 @@ public static Resource getAppDir(Resource resource) { * @param resource The Grails resource, which is a file inside the grails-app dir * @return The relative URL of the file inside the WEB-INF dir at deployment time or null if it cannot be established */ - public static String getRelativeInsideWebInf(Resource resource) { - if (resource == null) return null; + static String getRelativeInsideWebInf(Resource resource) { + if (resource == null) return null try { - String url = resource.getURL().toString(); - int i = url.indexOf(WEB_INF); + String url = resource.getURL().toString() + int i = url.indexOf(WEB_INF) if (i > -1) { - return url.substring(i); + return url.substring(i) } - Matcher m = PLUGIN_PATTERN.matcher(url); + Matcher m = PLUGIN_PATTERN.matcher(url) if (m.find()) { - return WEB_INF + m.group(1); + return WEB_INF + m.group(1) } - i = url.lastIndexOf(GRAILS_APP_DIR); + i = url.lastIndexOf(GRAILS_APP_DIR) if (i > -1) { - return WEB_INF + "/" + url.substring(i); + return WEB_INF + '/' + url.substring(i) } } catch (IOException e) { - return null; + return null } - return null; + return null } - private static final Pattern PLUGIN_RESOURCE_PATTERN = Pattern.compile(".+?/(plugins/.+?)/" + GRAILS_APP_DIR + "/.+"); + private static final Pattern PLUGIN_RESOURCE_PATTERN = Pattern.compile('.+?/(plugins/.+?)/' + GRAILS_APP_DIR + '/.+') /** * Retrieves the static resource path for the given Grails resource artifact (controller/taglib etc.) @@ -819,25 +813,25 @@ public static String getRelativeInsideWebInf(Resource resource) { * @param contextPath The additonal context path to prefix * @return The resource path */ - public static String getStaticResourcePathForResource(Resource resource, String contextPath) { + static String getStaticResourcePathForResource(Resource resource, String contextPath) { - if (contextPath == null) contextPath = ""; - if (resource == null) return contextPath; + if (contextPath == null) contextPath = '' + if (resource == null) return contextPath - String url; + String url try { - url = resource.getURL().toString(); + url = resource.getURL().toString() } catch (IOException e) { - return contextPath; + return contextPath } - Matcher m = PLUGIN_RESOURCE_PATTERN.matcher(url); + Matcher m = PLUGIN_RESOURCE_PATTERN.matcher(url) if (m.find()) { - return (contextPath.length() > 0 ? contextPath + "/" : "") + m.group(1); + return (contextPath.length() > 0 ? contextPath + '/' : '') + m.group(1) } - return contextPath; + return contextPath } /** @@ -852,14 +846,14 @@ public static String getStaticResourcePathForResource(Resource resource, String * @param path The path to evaluate * @return The path relative to the root folder grails-app */ - public static String getPathFromRoot(String path) { - for (Pattern COMPILER_ROOT_PATTERN : COMPILER_ROOT_PATTERNS) { - Matcher m = COMPILER_ROOT_PATTERN.matcher(path); + static String getPathFromRoot(String path) { + for (Pattern COMPILER_ROOT_PATTERN in COMPILER_ROOT_PATTERNS) { + Matcher m = COMPILER_ROOT_PATTERN.matcher(path) if (m.find()) { - return m.group(m.groupCount() - 1); + return m.group(m.groupCount() - 1) } } - return null; + return null } /** @@ -871,24 +865,24 @@ public static String getPathFromRoot(String path) { * @param path The path * @return The path relative to the base directory or null if it can't be established */ - public static String getPathFromBaseDir(String path) { - int i = path.indexOf("grails-app/"); + static String getPathFromBaseDir(String path) { + int i = path.indexOf('grails-app/') if (i > -1) { - return path.substring(i + 11); + return path.substring(i + 11) } else { try { - File baseDir = BuildSettings.BASE_DIR; - String basePath = baseDir != null ? baseDir.getCanonicalPath() : null; + File baseDir = BuildSettings.BASE_DIR + String basePath = baseDir != null ? baseDir.getCanonicalPath() : null if (basePath != null) { - String canonicalPath = new File(path).getCanonicalPath(); - return canonicalPath.contains(basePath) ? canonicalPath.substring(basePath.length() + 1) : canonicalPath; + String canonicalPath = new File(path).getCanonicalPath() + return canonicalPath.contains(basePath) ? canonicalPath.substring(basePath.length() + 1) : canonicalPath } - } catch (IOException e) { + } catch (IOException ignored) { // ignore } } - return null; + return null } /** @@ -900,15 +894,15 @@ public static String getPathFromBaseDir(String path) { * @param path The path * @return The domain or null if not known */ - public static String getArtefactDirectory(String path) { + static String getArtefactDirectory(String path) { if (path != null) { - final Matcher matcher = RESOURCE_PATH_PATTERN.matcher(path); + final Matcher matcher = RESOURCE_PATH_PATTERN.matcher(path) if (matcher.find()) { - return matcher.group(1); + return matcher.group(1) } } - return null; + return null } /** @@ -936,48 +930,49 @@ public static String getArtefactDirectory(String path) { * @param pieces Strings to concatenate together into a uri * @return a uri */ - public static String appendPiecesForUri(String... pieces) { - if (pieces == null || pieces.length == 0) return ""; + static String appendPiecesForUri(String... pieces) { + if (pieces == null || pieces.length == 0) return '' // join parts && strip double slashes - StringBuilder builder = new StringBuilder(16 * pieces.length); - char previous = 0; + StringBuilder builder = new StringBuilder(16 * pieces.length) + char previous = 0 for (int i = 0; i < pieces.length; i++) { - String piece = pieces[i]; + String piece = pieces[i] if (piece != null && piece.length() > 0) { for (int j = 0, maxlen = piece.length(); j < maxlen; j++) { - char current = piece.charAt(j); + char current = piece.charAt(j) if (!(previous == '/' && current == '/')) { - builder.append(current); - previous = current; + builder.append(current) + previous = current } } if (i + 1 < pieces.length && previous != '/') { - builder.append('/'); - previous = '/'; + builder.append('/') + previous = '/' } } } - return builder.toString(); + return builder.toString() } - @SuppressWarnings("unchecked") - public static Object instantiateFromConfig(ConfigObject config, String configKey, String defaultClassName) + @SuppressWarnings('unchecked') + static Object instantiateFromConfig(ConfigObject config, String configKey, String defaultClassName) throws InstantiationException, IllegalAccessException, ClassNotFoundException, LinkageError { - return instantiateFromFlatConfig(config.flatten(), configKey, defaultClassName); + return instantiateFromFlatConfig(config.flatten(), configKey, defaultClassName) } - public static Object instantiateFromFlatConfig(Map flatConfig, String configKey, String defaultClassName) + static Object instantiateFromFlatConfig(Map flatConfig, String configKey, String defaultClassName) throws InstantiationException, IllegalAccessException, ClassNotFoundException, LinkageError { - String className = defaultClassName; - Object configName = flatConfig.get(configKey); + String className = defaultClassName + Object configName = flatConfig.get(configKey) if (configName instanceof CharSequence) { - className = configName.toString(); + className = configName.toString() } - return forName(className, DefaultResourceLoader.getDefaultClassLoader()).newInstance(); + return forName(className, DefaultResourceLoader.getDefaultClassLoader()).newInstance() } private static Class forName(String className, ClassLoader defaultClassLoader) throws ClassNotFoundException { - return defaultClassLoader.loadClass(className); + return defaultClassLoader.loadClass(className) } + } diff --git a/grails-gradle/model/src/main/groovy/org/grails/io/support/Resource.java b/grails-gradle/model/src/main/groovy/org/grails/io/support/Resource.groovy similarity index 86% rename from grails-gradle/model/src/main/groovy/org/grails/io/support/Resource.java rename to grails-gradle/model/src/main/groovy/org/grails/io/support/Resource.groovy index 431dd9e48ce..63f27ea42a6 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/io/support/Resource.java +++ b/grails-gradle/model/src/main/groovy/org/grails/io/support/Resource.groovy @@ -16,13 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.io.support; - -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.URI; -import java.net.URL; +package org.grails.io.support /** * Interface for a resource descriptor that abstracts from the actual @@ -38,13 +32,13 @@ * @see #getURI() * @see #getFile() */ -public interface Resource { +interface Resource { /** * An input stream * @return The input stream */ - InputStream getInputStream() throws IOException; + InputStream getInputStream() throws IOException /** * Return whether this resource actually exists in physical form. @@ -52,7 +46,7 @@ public interface Resource { * existence of a Resource handle only guarantees a * valid descriptor handle. */ - boolean exists(); + boolean exists() /** * Return whether the contents of this resource can be read, @@ -63,42 +57,42 @@ public interface Resource { * that the resource content cannot be read. * @see #getInputStream() */ - boolean isReadable(); + boolean isReadable() /** * Return a URL handle for this resource. * @throws java.io.IOException if the resource cannot be resolved as URL, * i.e. if the resource is not available as descriptor */ - URL getURL() throws IOException; + URL getURL() throws IOException /** * Return a URI handle for this resource. * @throws IOException if the resource cannot be resolved as URI, * i.e. if the resource is not available as descriptor */ - URI getURI() throws IOException; + URI getURI() throws IOException /** * Return a File handle for this resource. * @throws IOException if the resource cannot be resolved as absolute * file path, i.e. if the resource is not available in a file system */ - File getFile() throws IOException; + File getFile() throws IOException /** * Determine the content length for this resource. * @throws IOException if the resource cannot be resolved * (in the file system or as some other known physical resource type) */ - long contentLength() throws IOException; + long contentLength() throws IOException /** * Determine the last-modified timestamp for this resource. * @throws IOException if the resource cannot be resolved * (in the file system or as some other known physical resource type) */ - long lastModified() throws IOException; + long lastModified() throws IOException /** * Determine a filename for this resource, i.e. typically the last @@ -106,7 +100,7 @@ public interface Resource { *

Returns null if this type of resource does not * have a filename. */ - String getFilename(); + String getFilename() /** * Return a description for this resource, @@ -115,7 +109,7 @@ public interface Resource { * from their toString method. * @see java.lang.Object#toString() */ - String getDescription(); + String getDescription() /** * Creates a new resource relative to this one @@ -123,5 +117,6 @@ public interface Resource { * @param relativePath The relative path * @return The new resource */ - Resource createRelative(String relativePath); + Resource createRelative(String relativePath) + } diff --git a/grails-gradle/model/src/main/groovy/org/grails/io/support/ResourceLoader.java b/grails-gradle/model/src/main/groovy/org/grails/io/support/ResourceLoader.groovy similarity index 92% rename from grails-gradle/model/src/main/groovy/org/grails/io/support/ResourceLoader.java rename to grails-gradle/model/src/main/groovy/org/grails/io/support/ResourceLoader.groovy index a2180809a87..1bd8e5216ef 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/io/support/ResourceLoader.java +++ b/grails-gradle/model/src/main/groovy/org/grails/io/support/ResourceLoader.groovy @@ -16,7 +16,7 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.io.support; +package org.grails.io.support /** * Strategy interface for loading resources (e.. class path or file system @@ -30,10 +30,10 @@ * @author Juergen Hoeller * @since 10.03.2004 */ -public interface ResourceLoader { +interface ResourceLoader { /** Pseudo URL prefix for loading from the class path: "classpath:" */ - String CLASSPATH_URL_PREFIX = "classpath:"; + String CLASSPATH_URL_PREFIX = 'classpath:' /** * Return a Resource handle for the specified resource. @@ -52,7 +52,7 @@ public interface ResourceLoader { * @return a corresponding Resource handle * @see #CLASSPATH_URL_PREFIX */ - Resource getResource(String location); + Resource getResource(String location) /** * Expose the ClassLoader used by this ResourceLoader. @@ -61,5 +61,6 @@ public interface ResourceLoader { * on the thread context ClassLoader. * @return the ClassLoader (never null) */ - ClassLoader getClassLoader(); + ClassLoader getClassLoader() + } diff --git a/grails-gradle/model/src/main/groovy/org/grails/io/support/SpringIOUtils.java b/grails-gradle/model/src/main/groovy/org/grails/io/support/SpringIOUtils.groovy similarity index 61% rename from grails-gradle/model/src/main/groovy/org/grails/io/support/SpringIOUtils.java rename to grails-gradle/model/src/main/groovy/org/grails/io/support/SpringIOUtils.groovy index b107c03d06b..4701e223976 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/io/support/SpringIOUtils.java +++ b/grails-gradle/model/src/main/groovy/org/grails/io/support/SpringIOUtils.groovy @@ -16,41 +16,25 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.io.support; - -import java.io.BufferedInputStream; -import java.io.BufferedOutputStream; -import java.io.ByteArrayInputStream; -import java.io.ByteArrayOutputStream; -import java.io.Closeable; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.Reader; -import java.io.StringWriter; -import java.io.Writer; -import java.lang.reflect.Array; -import java.net.URL; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.HashMap; -import java.util.Map; - -import javax.xml.XMLConstants; -import javax.xml.parsers.ParserConfigurationException; -import javax.xml.parsers.SAXParser; -import javax.xml.parsers.SAXParserFactory; - -import groovy.xml.FactorySupport; -import groovy.xml.XmlSlurper; - -import org.slf4j.LoggerFactory; -import org.xml.sax.SAXException; - -import org.apache.grails.gradle.common.XmlParserFeature; +package org.grails.io.support + +import java.lang.reflect.Array +import java.security.MessageDigest +import java.security.NoSuchAlgorithmException + +import javax.xml.XMLConstants +import javax.xml.parsers.ParserConfigurationException +import javax.xml.parsers.SAXParser +import javax.xml.parsers.SAXParserFactory + +import groovy.transform.CompileStatic +import groovy.xml.FactorySupport +import groovy.xml.XmlSlurper + +import org.slf4j.LoggerFactory +import org.xml.sax.SAXException + +import org.apache.grails.gradle.common.XmlParserFeature /** * Simple utility methods for file and stream copying. @@ -65,21 +49,22 @@ * * @since 06.10.2003 */ -@SuppressWarnings("unchecked") -public class SpringIOUtils { +@SuppressWarnings('unchecked') +@CompileStatic +class SpringIOUtils { - @SuppressWarnings("rawtypes") - private static Map algorithms = new HashMap(); + @SuppressWarnings('rawtypes') + private static Map algorithms = new HashMap() static { - algorithms.put("md5", "MD5"); - algorithms.put("sha1", "SHA-1"); + algorithms.put('md5', 'MD5') + algorithms.put('sha1', 'SHA-1') } // byte to hex string converter - private static final char[] CHARS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', - 'a', 'b', 'c', 'd', 'e', 'f'}; - public static final int BUFFER_SIZE = 4096; + private static final char[] CHARS = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'a', 'b', 'c', 'd', 'e', 'f'] as char[] + public static final int BUFFER_SIZE = 4096 /** * Convert a byte[] array to readable string format. This makes the "hex" readable! @@ -88,61 +73,61 @@ public class SpringIOUtils { * @param in * byte[] buffer to convert to string format */ - public static String byteArrayToHexString(byte[] in) { - byte ch = 0x00; + static String byteArrayToHexString(byte[] in) { + byte ch = 0x00 if (in == null || in.length <= 0) { - return null; + return null } - StringBuilder out = new StringBuilder(in.length * 2); + StringBuilder out = new StringBuilder(in.length * 2) //CheckStyle:MagicNumber OFF for (int i = 0; i < in.length; i++) { - ch = (byte) (in[i] & 0xF0); // Strip off high nibble - ch = (byte) (ch >>> 4); // shift the bits down - ch = (byte) (ch & 0x0F); // must do this is high order bit is on! + ch = (byte) (in[i] & 0xF0) // Strip off high nibble + ch = (byte) (ch >>> 4) // shift the bits down + ch = (byte) (ch & 0x0F) // must do this is high order bit is on! - out.append(CHARS[ch]); // convert the nibble to a String Character - ch = (byte) (in[i] & 0x0F); // Strip off low nibble - out.append(CHARS[ch]); // convert the nibble to a String Character + out.append(CHARS[ch]) // convert the nibble to a String Character + ch = (byte) (in[i] & 0x0F) // Strip off low nibble + out.append(CHARS[ch]) // convert the nibble to a String Character } //CheckStyle:MagicNumber ON - return out.toString(); + return out.toString() } - public static String computeChecksum(File f, String algorithm) throws IOException { - return byteArrayToHexString(compute(f, algorithm)); + static String computeChecksum(File f, String algorithm) throws IOException { + return byteArrayToHexString(compute(f, algorithm)) } private static byte[] compute(File f, String algorithm) throws IOException { - InputStream is = new FileInputStream(f); + InputStream is = new FileInputStream(f) try { - MessageDigest md = getMessageDigest(algorithm); - md.reset(); + MessageDigest md = getMessageDigest(algorithm) + md.reset() - byte[] buf = new byte[BUFFER_SIZE]; - int len = 0; + byte[] buf = new byte[BUFFER_SIZE] + int len = 0 while ((len = is.read(buf)) != -1) { - md.update(buf, 0, len); + md.update(buf, 0, len) } - return md.digest(); + return md.digest() } finally { - is.close(); + is.close() } } private static MessageDigest getMessageDigest(String algorithm) { - String mdAlgorithm = (String) algorithms.get(algorithm); + String mdAlgorithm = (String) algorithms.get(algorithm) if (mdAlgorithm == null) { - throw new IllegalArgumentException("unknown algorithm " + algorithm); + throw new IllegalArgumentException('unknown algorithm ' + algorithm) } try { - return MessageDigest.getInstance(mdAlgorithm); + return MessageDigest.getInstance(mdAlgorithm) } catch (NoSuchAlgorithmException e) { - throw new IllegalArgumentException("unknown algorithm " + algorithm); + throw new IllegalArgumentException('unknown algorithm ' + algorithm) } } @@ -153,20 +138,20 @@ private static MessageDigest getMessageDigest(String algorithm) { * @param array2 The source array * @return The new array */ - public static Object[] addAll(Object[] array1, Object[] array2) { - Object[] joinedArray = (Object[]) Array.newInstance(array1.getClass().getComponentType(), array1.length + array2.length); - System.arraycopy(array1, 0, joinedArray, 0, array1.length); + static Object[] addAll(Object[] array1, Object[] array2) { + Object[] joinedArray = (Object[]) Array.newInstance(array1.getClass().getComponentType(), array1.length + array2.length) + System.arraycopy(array1, 0, joinedArray, 0, array1.length) try { - System.arraycopy(array2, 0, joinedArray, array1.length, array2.length); + System.arraycopy(array2, 0, joinedArray, array1.length, array2.length) } catch (ArrayStoreException ase) { - final Class type1 = array1.getClass().getComponentType(); - final Class type2 = array2.getClass().getComponentType(); + final Class type1 = array1.getClass().getComponentType() + final Class type2 = array2.getClass().getComponentType() if (!type1.isAssignableFrom(type2)) { - throw new IllegalArgumentException("Cannot store " + type2.getName() + " in an array of " + type1.getName()); + throw new IllegalArgumentException('Cannot store ' + type2.getName() + ' in an array of ' + type1.getName()) } - throw ase; // No, so rethrow original + throw ase // No, so rethrow original } - return joinedArray; + return joinedArray } //--------------------------------------------------------------------- @@ -181,12 +166,12 @@ public static Object[] addAll(Object[] array1, Object[] array2) { * @param resources The resources to copy * @param targetDir The target directory */ - public static void copyAll(Resource base, Resource[] resources, File targetDir) throws IOException { - final URL baseUrl = base.getURL(); - for (Resource resource : resources) { - final InputStream input = resource.getInputStream(); - final File target = new File(targetDir, resource.getURL().toString().substring(baseUrl.toString().length())); - copy(new BufferedInputStream(input), new BufferedOutputStream(new FileOutputStream(target))); + static void copyAll(Resource base, Resource[] resources, File targetDir) throws IOException { + final URL baseUrl = base.getURL() + for (Resource resource in resources) { + final InputStream input = resource.getInputStream() + final File target = new File(targetDir, resource.getURL().toString().substring(baseUrl.toString().length())) + copy(new BufferedInputStream(input), new BufferedOutputStream(new FileOutputStream(target))) } } @@ -197,11 +182,11 @@ public static void copyAll(Resource base, Resource[] resources, File targetDir) * @return the number of bytes copied * @throws java.io.IOException in case of I/O errors */ - public static int copy(File in, File out) throws IOException { - assert in != null : "No input File specified"; - assert out != null : "No output File specified"; + static int copy(File in, File out) throws IOException { + assert in != null : 'No input File specified' + assert out != null : 'No output File specified' return copy(new BufferedInputStream(new FileInputStream(in)), - new BufferedOutputStream(new FileOutputStream(out))); + new BufferedOutputStream(new FileOutputStream(out))) } /** @@ -211,11 +196,11 @@ public static int copy(File in, File out) throws IOException { * @return the number of bytes copied * @throws java.io.IOException in case of I/O errors */ - public static int copy(Resource in, File out) throws IOException { - assert in != null : "No input File specified"; - assert out != null : "No output File specified"; + static int copy(Resource in, File out) throws IOException { + assert in != null : 'No input File specified' + assert out != null : 'No output File specified' return copy(new BufferedInputStream(in.getInputStream()), - new BufferedOutputStream(new FileOutputStream(out))); + new BufferedOutputStream(new FileOutputStream(out))) } /** @@ -224,12 +209,12 @@ public static int copy(Resource in, File out) throws IOException { * @param out the file to copy to * @throws IOException in case of I/O errors */ - public static void copy(byte[] in, File out) throws IOException { - assert in != null : "No input byte array specified"; - assert out != null : "No output File specified"; - ByteArrayInputStream inStream = new ByteArrayInputStream(in); - OutputStream outStream = new BufferedOutputStream(new FileOutputStream(out)); - copy(inStream, outStream); + static void copy(byte[] in, File out) throws IOException { + assert in != null : 'No input byte array specified' + assert out != null : 'No output File specified' + ByteArrayInputStream inStream = new ByteArrayInputStream(in) + OutputStream outStream = new BufferedOutputStream(new FileOutputStream(out)) + copy(inStream, outStream) } /** @@ -238,10 +223,10 @@ public static void copy(byte[] in, File out) throws IOException { * @return the new byte array that has been copied to * @throws IOException in case of I/O errors */ - public static byte[] copyToByteArray(File in) throws IOException { - assert in != null : "No input File specified"; + static byte[] copyToByteArray(File in) throws IOException { + assert in != null : 'No input File specified' - return copyToByteArray(new BufferedInputStream(new FileInputStream(in))); + return copyToByteArray(new BufferedInputStream(new FileInputStream(in))) } //--------------------------------------------------------------------- @@ -256,30 +241,30 @@ public static byte[] copyToByteArray(File in) throws IOException { * @return the number of bytes copied * @throws IOException in case of I/O errors */ - public static int copy(InputStream in, OutputStream out) throws IOException { - assert in != null : "No input stream specified"; - assert out != null : "No output stream specified"; + static int copy(InputStream in, OutputStream out) throws IOException { + assert in != null : 'No input stream specified' + assert out != null : 'No output stream specified' try { - int byteCount = 0; - byte[] buffer = new byte[BUFFER_SIZE]; - int bytesRead = -1; + int byteCount = 0 + byte[] buffer = new byte[BUFFER_SIZE] + int bytesRead = -1 while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - byteCount += bytesRead; + out.write(buffer, 0, bytesRead) + byteCount += bytesRead } - out.flush(); - return byteCount; + out.flush() + return byteCount } finally { try { - in.close(); + in.close() } - catch (IOException ex) { + catch (IOException ignored) { } try { - out.close(); + out.close() } - catch (IOException ex) { + catch (IOException ignored) { } } } @@ -291,17 +276,17 @@ public static int copy(InputStream in, OutputStream out) throws IOException { * @param out the OutputStream to copy to * @throws IOException in case of I/O errors */ - public static void copy(byte[] in, OutputStream out) throws IOException { - assert in != null : "No input byte array specified"; - assert out != null : "No output stream specified"; + static void copy(byte[] in, OutputStream out) throws IOException { + assert in != null : 'No input byte array specified' + assert out != null : 'No output stream specified' try { - out.write(in); + out.write(in) } finally { try { - out.close(); + out.close() } - catch (IOException ex) { + catch (IOException ignored) { } } } @@ -313,10 +298,10 @@ public static void copy(byte[] in, OutputStream out) throws IOException { * @return the new byte array that has been copied to * @throws IOException in case of I/O errors */ - public static byte[] copyToByteArray(InputStream in) throws IOException { - ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE); - copy(in, out); - return out.toByteArray(); + static byte[] copyToByteArray(InputStream in) throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(BUFFER_SIZE) + copy(in, out) + return out.toByteArray() } //--------------------------------------------------------------------- @@ -331,31 +316,31 @@ public static byte[] copyToByteArray(InputStream in) throws IOException { * @return the number of characters copied * @throws IOException in case of I/O errors */ - public static int copy(Reader in, Writer out) throws IOException { - assert in != null : "No input Reader specified"; - assert out != null : "No output Writer specified"; + static int copy(Reader in, Writer out) throws IOException { + assert in != null : 'No input Reader specified' + assert out != null : 'No output Writer specified' try { - int byteCount = 0; - char[] buffer = new char[BUFFER_SIZE]; - int bytesRead = -1; + int byteCount = 0 + char[] buffer = new char[BUFFER_SIZE] + int bytesRead = -1 while ((bytesRead = in.read(buffer)) != -1) { - out.write(buffer, 0, bytesRead); - byteCount += bytesRead; + out.write(buffer, 0, bytesRead) + byteCount += bytesRead } - out.flush(); - return byteCount; + out.flush() + return byteCount } finally { try { - in.close(); + in.close() } - catch (IOException ex) { + catch (IOException ignored) { } try { - out.close(); + out.close() } - catch (IOException ex) { + catch (IOException ignored) { } } } @@ -367,18 +352,18 @@ public static int copy(Reader in, Writer out) throws IOException { * @param out the Writer to copy to * @throws IOException in case of I/O errors */ - public static void copy(String in, Writer out) throws IOException { - assert in != null : "No input String specified"; - assert out != null : "No output Writer specified"; + static void copy(String in, Writer out) throws IOException { + assert in != null : 'No input String specified' + assert out != null : 'No output Writer specified' try { - out.write(in); + out.write(in) } finally { try { - out.close(); + out.close() } - catch (IOException ex) { + catch (IOException ignored) { } } } @@ -388,11 +373,11 @@ public static void copy(String in, Writer out) throws IOException { * * @param closeable The closeable */ - public static void closeQuietly(Closeable closeable) { + static void closeQuietly(Closeable closeable) { try { if (closeable != null) - closeable.close(); - } catch (IOException e) { + closeable.close() + } catch (IOException ignored) { // ignore } } @@ -404,10 +389,10 @@ public static void closeQuietly(Closeable closeable) { * @return the String that has been copied to * @throws IOException in case of I/O errors */ - public static String copyToString(Reader in) throws IOException { - StringWriter out = new StringWriter(); - copy(in, out); - return out.toString(); + static String copyToString(Reader in) throws IOException { + StringWriter out = new StringWriter() + copy(in, out) + return out.toString() } /** @@ -419,8 +404,8 @@ public static String copyToString(Reader in) throws IOException { * * @return a namespace-aware, non-validating slurper that rejects {@code DOCTYPE} declarations */ - public static XmlSlurper createXmlSlurper() throws ParserConfigurationException, SAXException { - return createXmlSlurper(false); + static XmlSlurper createXmlSlurper() throws ParserConfigurationException, SAXException { + return createXmlSlurper(false) } /** @@ -441,8 +426,8 @@ public static XmlSlurper createXmlSlurper() throws ParserConfigurationException, * @return a namespace-aware, non-validating slurper with the hardening described above * @since 8.0.0 */ - public static XmlSlurper createXmlSlurper(boolean allowDocTypeDeclaration) throws ParserConfigurationException, SAXException { - return new XmlSlurper(newSAXParser(allowDocTypeDeclaration)); + static XmlSlurper createXmlSlurper(boolean allowDocTypeDeclaration) throws ParserConfigurationException, SAXException { + return new XmlSlurper(newSAXParser(allowDocTypeDeclaration)) } /** @@ -453,8 +438,8 @@ public static XmlSlurper createXmlSlurper(boolean allowDocTypeDeclaration) throw * * @return a namespace-aware, non-validating parser that rejects {@code DOCTYPE} declarations */ - public static SAXParser newSAXParser() throws ParserConfigurationException, SAXException { - return newSAXParser(false); + static SAXParser newSAXParser() throws ParserConfigurationException, SAXException { + return newSAXParser(false) } /** @@ -466,8 +451,8 @@ public static SAXParser newSAXParser() throws ParserConfigurationException, SAXE * @return a namespace-aware, non-validating parser * @since 8.0.0 */ - public static SAXParser newSAXParser(boolean allowDocTypeDeclaration) throws ParserConfigurationException, SAXException { - return parserFactory(allowDocTypeDeclaration).newSAXParser(); + static SAXParser newSAXParser(boolean allowDocTypeDeclaration) throws ParserConfigurationException, SAXException { + return parserFactory(allowDocTypeDeclaration).newSAXParser() } /** @@ -476,49 +461,49 @@ public static SAXParser newSAXParser(boolean allowDocTypeDeclaration) throws Par *

{@link XmlParserFeature#DISALLOW_DOCTYPE_DECL} is handled separately because it is the * one feature that differs between the two parsers; see {@link #createXmlSlurper(boolean)}. */ - private static final XmlParserFeature[] DISABLED_PARSER_FEATURES = { + private static final XmlParserFeature[] DISABLED_PARSER_FEATURES = [ XmlParserFeature.EXTERNAL_GENERAL_ENTITIES, XmlParserFeature.EXTERNAL_PARAMETER_ENTITIES, XmlParserFeature.LOAD_DTD_GRAMMAR, XmlParserFeature.LOAD_EXTERNAL_DTD - }; + ] as XmlParserFeature[] - private static volatile SAXParserFactory strictParserFactory; + private static volatile SAXParserFactory strictParserFactory - private static volatile SAXParserFactory docTypeParserFactory; + private static volatile SAXParserFactory docTypeParserFactory private static SAXParserFactory parserFactory(boolean allowDocTypeDeclaration) throws ParserConfigurationException { if (allowDocTypeDeclaration) { - SAXParserFactory factory = docTypeParserFactory; + SAXParserFactory factory = docTypeParserFactory if (factory == null) { - factory = buildParserFactory(true); - docTypeParserFactory = factory; + factory = buildParserFactory(true) + docTypeParserFactory = factory } - return factory; + return factory } - SAXParserFactory factory = strictParserFactory; + SAXParserFactory factory = strictParserFactory if (factory == null) { - factory = buildParserFactory(false); - strictParserFactory = factory; + factory = buildParserFactory(false) + strictParserFactory = factory } - return factory; + return factory } private static SAXParserFactory buildParserFactory(boolean allowDocTypeDeclaration) throws ParserConfigurationException { - SAXParserFactory factory = FactorySupport.createSaxParserFactory(); - factory.setNamespaceAware(true); - factory.setValidating(false); + SAXParserFactory factory = FactorySupport.createSaxParserFactory() + factory.setNamespaceAware(true) + factory.setValidating(false) try { - factory.setXIncludeAware(false); - } catch (UnsupportedOperationException e) { + factory.setXIncludeAware(false) + } catch (UnsupportedOperationException ignored) { // a parser without XInclude support cannot expand an include either } - setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true); - setFeature(factory, XmlParserFeature.DISALLOW_DOCTYPE_DECL.getFeatureName(), !allowDocTypeDeclaration); + setFeature(factory, XMLConstants.FEATURE_SECURE_PROCESSING, true) + setFeature(factory, XmlParserFeature.DISALLOW_DOCTYPE_DECL.featureName, !allowDocTypeDeclaration) for (XmlParserFeature feature : DISABLED_PARSER_FEATURES) { - setFeature(factory, feature.getFeatureName(), false); + setFeature(factory, feature.featureName, false) } - return factory; + return factory } /** @@ -532,11 +517,12 @@ private static SAXParserFactory buildParserFactory(boolean allowDocTypeDeclarati */ private static void setFeature(SAXParserFactory factory, String name, boolean value) { try { - factory.setFeature(name, value); + factory.setFeature(name, value) } catch (ParserConfigurationException | SAXException e) { - LoggerFactory.getLogger(SpringIOUtils.class).warn( - "XML parser factory [{}] does not support feature [{}]: {}", - factory.getClass().getName(), name, e.getMessage()); + LoggerFactory.getLogger(SpringIOUtils).warn( + 'XML parser factory [{}] does not support feature [{}]: {}', + factory.getClass().getName(), name, e.getMessage()) } } + } diff --git a/grails-gradle/model/src/main/groovy/org/grails/io/support/UrlResource.java b/grails-gradle/model/src/main/groovy/org/grails/io/support/UrlResource.groovy similarity index 66% rename from grails-gradle/model/src/main/groovy/org/grails/io/support/UrlResource.java rename to grails-gradle/model/src/main/groovy/org/grails/io/support/UrlResource.groovy index f2ba1989d9f..bfdaaecb5b9 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/io/support/UrlResource.java +++ b/grails-gradle/model/src/main/groovy/org/grails/io/support/UrlResource.groovy @@ -16,16 +16,9 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.io.support; +package org.grails.io.support -import java.io.File; -import java.io.IOException; -import java.io.InputStream; -import java.net.HttpURLConnection; -import java.net.MalformedURLException; -import java.net.URI; -import java.net.URL; -import java.net.URLConnection; +import groovy.transform.CompileStatic /** * Resource implementation for java.net.URL locators. @@ -36,31 +29,32 @@ * @since 28.12.2003 * @see java.net.URL */ -public class UrlResource extends AbstractFileResolvingResource { +@CompileStatic +class UrlResource extends AbstractFileResolvingResource { /** * Original URL, used for actual access. */ - private final URL url; + private final URL url /** * Cleaned URL (with normalized path), used for comparisons. */ - private final URL cleanedUrl; + private final URL cleanedUrl /** * Original URI, if available; used for URI and File access. */ - private final URI uri; + private final URI uri /** * Create a new UrlResource. * @param url a URL */ - public UrlResource(URL url) { - this.url = url; - cleanedUrl = getCleanedUrl(url, url.toString()); - uri = null; + UrlResource(URL url) { + this.url = url + cleanedUrl = getCleanedUrl(url, url.toString()) + uri = null } /** @@ -68,10 +62,10 @@ public UrlResource(URL url) { * @param uri a URI * @throws java.net.MalformedURLException if the given URL path is not valid */ - public UrlResource(URI uri) throws MalformedURLException { - url = uri.toURL(); - cleanedUrl = getCleanedUrl(url, uri.toString()); - this.uri = uri; + UrlResource(URI uri) throws MalformedURLException { + url = uri.toURL() + cleanedUrl = getCleanedUrl(url, uri.toString()) + this.uri = uri } /** @@ -79,10 +73,10 @@ public UrlResource(URI uri) throws MalformedURLException { * @param path a URL path * @throws MalformedURLException if the given URL path is not valid */ - public UrlResource(String path) throws MalformedURLException { - url = new URL(path); - cleanedUrl = getCleanedUrl(url, path); - uri = null; + UrlResource(String path) throws MalformedURLException { + url = new URL(path) + cleanedUrl = getCleanedUrl(url, path) + uri = null } /** @@ -93,12 +87,12 @@ public UrlResource(String path) throws MalformedURLException { */ private URL getCleanedUrl(URL originalUrl, String originalPath) { try { - return new URL(GrailsResourceUtils.cleanPath(originalPath)); + return new URL(GrailsResourceUtils.cleanPath(originalPath)) } catch (MalformedURLException ex) { // Cleaned URL path cannot be converted to URL // -> take original URL. - return originalUrl; + return originalUrl } } @@ -110,41 +104,41 @@ private URL getCleanedUrl(URL originalUrl, String originalPath) { * @see java.net.URLConnection#setUseCaches(boolean) * @see java.net.URLConnection#getInputStream() */ - public InputStream getInputStream() throws IOException { - URLConnection con = url.openConnection(); - useCachesIfNecessary(con); + InputStream getInputStream() throws IOException { + URLConnection con = url.openConnection() + useCachesIfNecessary(con) try { - return con.getInputStream(); + return con.getInputStream() } catch (IOException ex) { // Close the HTTP connection (if applicable). if (con instanceof HttpURLConnection) { - ((HttpURLConnection) con).disconnect(); + ((HttpURLConnection) con).disconnect() } - throw ex; + throw ex } } private static void useCachesIfNecessary(URLConnection con) { - con.setUseCaches(con.getClass().getName().startsWith("JNLP")); + con.setUseCaches(con.getClass().getName().startsWith('JNLP')) } /** * This implementation returns the underlying URL reference. */ - public URL getURL() throws IOException { - return url; + URL getURL() throws IOException { + return url } /** * This implementation returns the underlying URI directly, * if possible. */ - public URI getURI() throws IOException { + URI getURI() throws IOException { if (uri != null) { - return uri; + return uri } - return getFile().toURI(); + return getFile().toURI() } /** @@ -152,8 +146,8 @@ public URI getURI() throws IOException { * provided that it refers to a file in the file system. */ @Override - public File getFile() throws IOException { - return uri == null ? super.getFile() : super.getFile(uri); + File getFile() throws IOException { + return uri == null ? super.getFile() : super.getFile(uri) } /** @@ -161,14 +155,14 @@ public File getFile() throws IOException { * relative to the path of the underlying URL of this resource descriptor. * @see java.net.URL#URL(java.net.URL, String) */ - public Resource createRelative(String relativePath) { - if (relativePath.startsWith("/")) { - relativePath = relativePath.substring(1); + Resource createRelative(String relativePath) { + if (relativePath.startsWith('/')) { + relativePath = relativePath.substring(1) } try { - return new UrlResource(new URL(url, relativePath)); + return new UrlResource(new URL(url, relativePath)) } catch (MalformedURLException e) { - return null; + return null } } @@ -177,36 +171,37 @@ public Resource createRelative(String relativePath) { * @see java.net.URL#getFile() * @see java.io.File#getName() */ - public String getFilename() { - return new File(url.getFile()).getName(); + String getFilename() { + return new File(url.getFile()).getName() } /** * This implementation returns a description that includes the URL. */ - public String getDescription() { - return "URL [" + url + "]"; + String getDescription() { + return 'URL [' + url + ']' } /** * This implementation compares the underlying URL references. */ @Override - public boolean equals(Object obj) { - return (obj == this || - (obj instanceof UrlResource && cleanedUrl.equals(((UrlResource) obj).cleanedUrl))); + boolean equals(Object obj) { + return (this.is(obj) || + (obj instanceof UrlResource && cleanedUrl.equals(((UrlResource) obj).cleanedUrl))) } /** * This implementation returns the hash code of the underlying URL reference. */ @Override - public int hashCode() { - return cleanedUrl.hashCode(); + int hashCode() { + return cleanedUrl.hashCode() } @Override - public String toString() { - return getDescription(); + String toString() { + return getDescription() } + } diff --git a/grails-gradle/model/src/test/groovy/grails/util/CollectionUtilsSpec.groovy b/grails-gradle/model/src/test/groovy/grails/util/CollectionUtilsSpec.groovy new file mode 100644 index 00000000000..048626f3608 --- /dev/null +++ b/grails-gradle/model/src/test/groovy/grails/util/CollectionUtilsSpec.groovy @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package grails.util + +import spock.lang.Specification + +class CollectionUtilsSpec extends Specification { + + void 'newMap builds a map from alternating keys and values'() { + expect: + CollectionUtils.newMap('a', 1, 'b', 2) == [a: 1, b: 2] + } + + void 'newMap returns an empty map for null input'() { + expect: + CollectionUtils.newMap((Object[]) null) == [:] + } + + void 'newMap rejects an odd number of arguments'() { + when: + CollectionUtils.newMap('a', 1, 'b') + + then: + thrown(IllegalArgumentException) + } + + void 'newSet builds a set from varargs'() { + expect: + CollectionUtils.newSet('a', 'b', 'a') == ['a', 'b'] as Set + } + + void 'newSet returns an empty set for null input'() { + expect: + CollectionUtils.newSet((Object[]) null).isEmpty() + } + + void 'newList builds a list from varargs'() { + expect: + CollectionUtils.newList('a', 'b') == ['a', 'b'] + } + + void 'newList returns an empty list for null input'() { + expect: + CollectionUtils.newList((Object[]) null).isEmpty() + } + + void 'getOrCreateChildMap returns the existing child map'() { + given: + Map child = [x: 1] + Map parent = [foo: child] + + expect: + CollectionUtils.getOrCreateChildMap(parent, 'foo').is(child) + } + + void 'getOrCreateChildMap returns a new empty map when the key is absent'() { + given: + Map parent = [:] + + expect: + CollectionUtils.getOrCreateChildMap(parent, 'missing') == [:] + } + + void 'getOrCreateChildMap returns a new empty map when the value is not a map'() { + given: + Map parent = [foo: 'not a map'] + + expect: + CollectionUtils.getOrCreateChildMap(parent, 'foo') == [:] + } + +} diff --git a/grails-gradle/model/src/test/groovy/grails/util/HolderSpec.groovy b/grails-gradle/model/src/test/groovy/grails/util/HolderSpec.groovy new file mode 100644 index 00000000000..3d2857b28c4 --- /dev/null +++ b/grails-gradle/model/src/test/groovy/grails/util/HolderSpec.groovy @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package grails.util + +import spock.lang.Specification + +class HolderSpec extends Specification { + + void 'get returns null before anything is set'() { + given: + Holder holder = new Holder<>('test') + + expect: + holder.get() == null + } + + void 'set then get returns the stored value'() { + given: + Holder holder = new Holder<>('test') + + when: + holder.set('hello') + + then: + holder.get() == 'hello' + } + + void 'set with null clears the value'() { + given: + Holder holder = new Holder<>('test') + holder.set('hello') + + when: + holder.set(null) + + then: + holder.get() == null + } + + void 'get(mappedOnly=true) does not fall back to the singleton value when unmapped'() { + given: + Holder holder = new Holder<>('test') + holder.set('hello') + + expect: + holder.get(true) == 'hello' + } + +} diff --git a/grails-gradle/model/src/test/groovy/grails/util/SupplierUtilSpec.groovy b/grails-gradle/model/src/test/groovy/grails/util/SupplierUtilSpec.groovy new file mode 100644 index 00000000000..95ca994c4d7 --- /dev/null +++ b/grails-gradle/model/src/test/groovy/grails/util/SupplierUtilSpec.groovy @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package grails.util + +import java.util.function.Supplier + +import spock.lang.Specification + +class SupplierUtilSpec extends Specification { + + void 'memoized only invokes the delegate supplier once'() { + given: + int callCount = 0 + Supplier actual = { -> callCount++; 'value' } as Supplier + Supplier memoized = SupplierUtil.memoized(actual) + + when: + String first = memoized.get() + String second = memoized.get() + + then: + first == 'value' + second == 'value' + callCount == 1 + } + + void 'memoizedNonEmpty caches a non-null, non-empty value'() { + given: + int callCount = 0 + Supplier actual = { -> callCount++; 'value' } as Supplier + Supplier memoized = SupplierUtil.memoizedNonEmpty(actual) + + when: + String first = memoized.get() + String second = memoized.get() + + then: + first == 'value' + second == 'value' + callCount == 1 + } + + void 'memoizedNonEmpty does not cache a null value'() { + given: + int callCount = 0 + Supplier actual = { -> callCount++; null } as Supplier + Supplier memoized = SupplierUtil.memoizedNonEmpty(actual) + + when: + memoized.get() + memoized.get() + + then: + callCount == 2 + } + + void 'memoizedNonEmpty does not cache an empty Optional'() { + given: + int callCount = 0 + Supplier> actual = { -> callCount++; Optional.empty() } as Supplier> + Supplier> memoized = SupplierUtil.memoizedNonEmpty(actual) + + when: + memoized.get() + memoized.get() + + then: + callCount == 2 + } + + void 'memoizedNonEmpty caches a present Optional'() { + given: + int callCount = 0 + Supplier> actual = { -> callCount++; Optional.of('value') } as Supplier> + Supplier> memoized = SupplierUtil.memoizedNonEmpty(actual) + + when: + Optional first = memoized.get() + Optional second = memoized.get() + + then: + first.get() == 'value' + second.get() == 'value' + callCount == 1 + } + +} diff --git a/grails-gradle/model/src/test/groovy/org/grails/build/parsing/OptionSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/build/parsing/OptionSpec.groovy new file mode 100644 index 00000000000..7c0f0547d4f --- /dev/null +++ b/grails-gradle/model/src/test/groovy/org/grails/build/parsing/OptionSpec.groovy @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package org.grails.build.parsing + +import spock.lang.Specification + +class OptionSpec extends Specification { + + void 'stores the given name and description'() { + when: + Option option = new Option('offline', 'Run offline') + + then: + option.name == 'offline' + option.description == 'Run offline' + } + + void 'defaults a null description to an empty string'() { + when: + Option option = new Option('offline', null) + + then: + option.description == '' + } + + void 'rejects a null name'() { + when: + new Option(null, 'desc') + + then: + thrown(IllegalArgumentException) + } + + void 'rejects an empty name'() { + when: + new Option('', 'desc') + + then: + thrown(IllegalArgumentException) + } + +} diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/ClassPathResourceSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/ClassPathResourceSpec.groovy new file mode 100644 index 00000000000..f3b16debfd3 --- /dev/null +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/ClassPathResourceSpec.groovy @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package org.grails.io.support + +import spock.lang.Specification + +class ClassPathResourceSpec extends Specification { + + void 'exists is true for a resource on the classpath'() { + expect: + new ClassPathResource('org/grails/io/support/Resource.class', ClassPathResourceSpec.classLoader).exists() + } + + void 'exists is false for a missing resource'() { + expect: + !new ClassPathResource('does/not/Exist.class', ClassPathResourceSpec.classLoader).exists() + } + + void 'getInputStream throws FileNotFoundException for a missing resource'() { + when: + new ClassPathResource('does/not/Exist.class', ClassPathResourceSpec.classLoader).getInputStream() + + then: + thrown(FileNotFoundException) + } + + void 'equals is true for the same instance'() { + given: + ClassPathResource resource = new ClassPathResource('foo.txt') + + expect: + resource == resource + } + + void 'equals is true for two resources with the same path and no classloader/class'() { + expect: + new ClassPathResource('foo.txt') == new ClassPathResource('foo.txt') + new ClassPathResource('foo.txt').hashCode() == new ClassPathResource('foo.txt').hashCode() + } + + void 'equals is true for two resources with the same path and same classloader'() { + given: + ClassLoader cl = ClassPathResourceSpec.classLoader + + expect: + new ClassPathResource('foo.txt', cl) == new ClassPathResource('foo.txt', cl) + } + + void 'equals is false for two resources with different paths'() { + expect: + new ClassPathResource('foo.txt') != new ClassPathResource('bar.txt') + } + + void 'equals is false for two resources with the same path but different classloaders'() { + given: + ClassLoader other = new URLClassLoader(new URL[0], null) + + expect: + new ClassPathResource('foo.txt', ClassPathResourceSpec.classLoader) != new ClassPathResource('foo.txt', other) + } + + void 'equals is false when compared against an unrelated object'() { + expect: + !new ClassPathResource('foo.txt').equals('not a resource') + } + + void 'getFilename returns the last path segment'() { + expect: + new ClassPathResource('org/grails/foo.txt').getFilename() == 'foo.txt' + } + + void 'a leading slash is stripped from the path'() { + expect: + new ClassPathResource('/foo.txt').getPath() == 'foo.txt' + } + + void 'createRelative resolves relative to the resource path'() { + given: + ClassPathResource resource = new ClassPathResource('org/grails/foo.txt') + + when: + Resource relative = resource.createRelative('bar.txt') + + then: + relative instanceof ClassPathResource + ((ClassPathResource) relative).getPath() == 'org/grails/bar.txt' + } + +} diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/DefaultResourceLoaderSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/DefaultResourceLoaderSpec.groovy new file mode 100644 index 00000000000..e5380a54e6b --- /dev/null +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/DefaultResourceLoaderSpec.groovy @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package org.grails.io.support + +import spock.lang.Specification + +class DefaultResourceLoaderSpec extends Specification { + + void 'getResource resolves a classpath: pseudo-URL to a ClassPathResource'() { + given: + DefaultResourceLoader loader = new DefaultResourceLoader() + + when: + Resource resource = loader.getResource('classpath:org/grails/io/support/Resource.class') + + then: + resource instanceof ClassPathResource + ((ClassPathResource) resource).getPath() == 'org/grails/io/support/Resource.class' + } + + void 'getResource resolves a real URL to a UrlResource'() { + given: + DefaultResourceLoader loader = new DefaultResourceLoader() + + when: + Resource resource = loader.getResource('file:///tmp/foo.txt') + + then: + resource instanceof UrlResource + } + + void 'getResource falls back to a classpath-relative resource for a plain path'() { + given: + DefaultResourceLoader loader = new DefaultResourceLoader() + + when: + Resource resource = loader.getResource('org/grails/io/support/Resource.class') + + then: + resource instanceof ClassPathResource + resource.exists() + } + + void 'getClassLoader defaults to the thread context class loader'() { + expect: + new DefaultResourceLoader().getClassLoader() != null + } + + void 'a custom classloader passed to the constructor is used'() { + given: + ClassLoader custom = new URLClassLoader(new URL[0], null) + + expect: + new DefaultResourceLoader(custom).getClassLoader().is(custom) + } + + void 'setClassLoader overrides the classloader used for classpath resolution'() { + given: + DefaultResourceLoader loader = new DefaultResourceLoader() + ClassLoader custom = new URLClassLoader(new URL[0], null) + + when: + loader.setClassLoader(custom) + + then: + loader.getClassLoader().is(custom) + } + +} diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/FileSystemResourceSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/FileSystemResourceSpec.groovy new file mode 100644 index 00000000000..652ebf2995b --- /dev/null +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/FileSystemResourceSpec.groovy @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package org.grails.io.support + +import spock.lang.Specification +import spock.lang.TempDir + +class FileSystemResourceSpec extends Specification { + + @TempDir + File tempDir + + void 'exists is true for a real file and false for a missing one'() { + given: + File real = new File(tempDir, 'real.txt') + real.text = 'hello' + + expect: + new FileSystemResource(real).exists() + !new FileSystemResource(new File(tempDir, 'missing.txt')).exists() + } + + void 'getInputStream reads the file contents'() { + given: + File real = new File(tempDir, 'real.txt') + real.text = 'hello world' + + expect: + new FileSystemResource(real).getInputStream().text == 'hello world' + } + + void 'equals is true for the same instance'() { + given: + FileSystemResource resource = new FileSystemResource(new File(tempDir, 'a.txt')) + + expect: + resource == resource + } + + void 'equals is true for two resources with the same cleaned path'() { + given: + File file = new File(tempDir, 'a.txt') + + expect: + new FileSystemResource(file) == new FileSystemResource(file) + new FileSystemResource(file).hashCode() == new FileSystemResource(file).hashCode() + } + + void 'equals is false for resources with different paths'() { + expect: + new FileSystemResource(new File(tempDir, 'a.txt')) != new FileSystemResource(new File(tempDir, 'b.txt')) + } + + void 'equals does not infinitely recurse for a distinct non-equal resource'() { + given: + FileSystemResource one = new FileSystemResource(new File(tempDir, 'a.txt')) + FileSystemResource two = new FileSystemResource(new File(tempDir, 'b.txt')) + + when: + boolean result = one.equals(two) + + then: + noExceptionThrown() + !result + } + + void 'constructor rejects a null File'() { + when: + new FileSystemResource((File) null) + + then: + thrown(IllegalArgumentException) + } + + void 'constructor rejects a null path'() { + when: + new FileSystemResource((String) null) + + then: + thrown(IllegalArgumentException) + } + + void 'getFilename returns the file name'() { + expect: + new FileSystemResource(new File(tempDir, 'foo.txt')).getFilename() == 'foo.txt' + } + + void 'createRelative resolves a sibling path'() { + given: + File base = new File(tempDir, 'dir/foo.txt') + FileSystemResource resource = new FileSystemResource(base) + + when: + Resource relative = resource.createRelative('bar.txt') + + then: + relative.getFilename() == 'bar.txt' + } + +} diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/GrailsResourceUtilsSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/GrailsResourceUtilsSpec.groovy new file mode 100644 index 00000000000..00db77632de --- /dev/null +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/GrailsResourceUtilsSpec.groovy @@ -0,0 +1,136 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package org.grails.io.support + +import spock.lang.Specification +import spock.lang.Unroll + +class GrailsResourceUtilsSpec extends Specification { + + void 'cleanPath normalizes .. and . segments'() { + expect: + GrailsResourceUtils.cleanPath('foo/bar/../baz') == 'foo/baz' + GrailsResourceUtils.cleanPath('foo/./bar') == 'foo/bar' + GrailsResourceUtils.cleanPath('foo\\bar') == 'foo/bar' + } + + void 'cleanPath preserves a scheme prefix'() { + expect: + GrailsResourceUtils.cleanPath('file:core/../core/io/Resource.class') == 'file:core/io/Resource.class' + } + + void 'cleanPath returns null for a null path'() { + expect: + GrailsResourceUtils.cleanPath(null) == null + } + + @Unroll + void 'getFilename extracts the last path segment for #path'() { + expect: + GrailsResourceUtils.getFilename(path) == expected + + where: + path | expected + 'foo/bar/baz.txt' | 'baz.txt' + 'baz.txt' | 'baz.txt' + null | null + } + + void 'classPackageAsResourcePath converts dots to slashes'() { + expect: + GrailsResourceUtils.classPackageAsResourcePath(GrailsResourceUtilsSpec) == 'org/grails/io/support' + } + + void 'classPackageAsResourcePath returns empty string for a null class'() { + expect: + GrailsResourceUtils.classPackageAsResourcePath(null) == '' + } + + void 'applyRelativePath replaces the last path segment'() { + expect: + GrailsResourceUtils.applyRelativePath('foo/bar/baz.txt', 'other.txt') == 'foo/bar/other.txt' + } + + void 'applyRelativePath returns the relative path when there is no separator'() { + expect: + GrailsResourceUtils.applyRelativePath('baz.txt', 'other.txt') == 'other.txt' + } + + void 'isFileURL is true for file: URLs and false for http: URLs'() { + expect: + GrailsResourceUtils.isFileURL(new URL('file:///tmp/foo.txt')) + !GrailsResourceUtils.isFileURL(new URL('https://example.org/foo.txt')) + } + + private static URL urlWithProtocol(String protocol, String spec) { + // "zip:"/"wsjar:" are not registered on a plain JVM - build the URL with a + // no-op handler so only the protocol name is exercised, not real resolution. + URLStreamHandler noopHandler = new URLStreamHandler() { + @Override + protected URLConnection openConnection(URL u) { + return null + } + } + return new URL(null, protocol + ':' + spec, noopHandler) + } + + void 'isJarURL is true for jar:/zip:/wsjar: protocols and false otherwise'() { + expect: + GrailsResourceUtils.isJarURL(new URL('jar:file:/foo.jar!/Bar.class')) + GrailsResourceUtils.isJarURL(urlWithProtocol('zip', 'file:/foo.jar!/Bar.class')) + GrailsResourceUtils.isJarURL(urlWithProtocol('wsjar', 'file:/foo.jar!/Bar.class')) + !GrailsResourceUtils.isJarURL(new URL('file:///tmp/foo.txt')) + } + + void 'isDomainClass matches a file under grails-app/domain'() { + given: + URL url = new URL('file:///project/grails-app/domain/com/example/Book.groovy') + + expect: + GrailsResourceUtils.isDomainClass(url) + } + + void 'isDomainClass is false for a file outside grails-app/domain'() { + given: + URL url = new URL('file:///project/grails-app/controllers/com/example/BookController.groovy') + + expect: + !GrailsResourceUtils.isDomainClass(url) + } + + void 'isDomainClass is false for a null URL'() { + expect: + !GrailsResourceUtils.isDomainClass(null) + } + + void 'isGrailsPath recognises the standard grails-app source roots'() { + expect: + GrailsResourceUtils.isGrailsPath('/project/grails-app/domain/com/example/Book.groovy') + GrailsResourceUtils.isGrailsPath('/project/grails-app/conf/spring/resources.groovy') + !GrailsResourceUtils.isGrailsPath('/project/build/classes/Helper.class') + } + + void 'COMPILER_ROOT_PATTERNS contains the spring scripts and resource path patterns'() { + expect: + GrailsResourceUtils.COMPILER_ROOT_PATTERNS.length == 2 + GrailsResourceUtils.COMPILER_ROOT_PATTERNS[1].matcher( + '/project/grails-app/controllers/com/example/BookController.groovy').find() + } + +} diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/SpringIOUtilsSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/SpringIOUtilsSpec.groovy index c3afb02351b..ac69111f954 100644 --- a/grails-gradle/model/src/test/groovy/org/grails/io/support/SpringIOUtilsSpec.groovy +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/SpringIOUtilsSpec.groovy @@ -57,6 +57,9 @@ class SpringIOUtilsSpec extends Specification { @TempDir Path tempDir + @TempDir + File tempFileDir + private String externalEntityDocument() { Path secret = tempDir.resolve('secret.txt') Files.writeString(secret, SECRET) @@ -124,6 +127,15 @@ class SpringIOUtilsSpec extends Specification { e.message.contains('DOCTYPE is disallowed') } + void 'newSAXParser returns a namespace-aware, non-validating parser'() { + when: + def parser = SpringIOUtils.newSAXParser() + + then: + parser.isNamespaceAware() + !parser.isValidating() + } + void 'asking for doctype tolerance parses a descriptor that declares one'() { when: def parsed = SpringIOUtils.createXmlSlurper(true).parseText(TLD) @@ -191,4 +203,112 @@ class SpringIOUtilsSpec extends Specification { where: allowDocType << [false, true] } + + void 'byteArrayToHexString converts bytes to lowercase hex'() { + expect: + SpringIOUtils.byteArrayToHexString([0x00, 0x0f, (byte) 0xff] as byte[]) == '000fff' + } + + void 'byteArrayToHexString returns null for a null or empty array'() { + expect: + SpringIOUtils.byteArrayToHexString(null) == null + SpringIOUtils.byteArrayToHexString(new byte[0]) == null + } + + void 'copy(byte[], File) writes the bytes to the file'() { + given: + File out = new File(tempFileDir, 'out.bin') + + when: + SpringIOUtils.copy('hello'.bytes, out) + + then: + out.text == 'hello' + } + + void 'copy(File, File) copies file contents'() { + given: + File source = new File(tempFileDir, 'source.txt') + source.text = 'copy me' + File target = new File(tempFileDir, 'target.txt') + + when: + int count = SpringIOUtils.copy(source, target) + + then: + target.text == 'copy me' + count == 'copy me'.bytes.length + } + + void 'copyToByteArray(File) reads the whole file'() { + given: + File source = new File(tempFileDir, 'source.txt') + source.text = 'bytes here' + + expect: + new String(SpringIOUtils.copyToByteArray(source)) == 'bytes here' + } + + void 'copyToString(Reader) reads the whole reader'() { + expect: + SpringIOUtils.copyToString(new StringReader('reader contents')) == 'reader contents' + } + + void 'copy(String, Writer) writes the string and closes the writer'() { + given: + StringWriter writer = new StringWriter() + + when: + SpringIOUtils.copy('written text', writer) + + then: + writer.toString() == 'written text' + } + + void 'computeChecksum computes an md5 digest'() { + given: + File source = new File(tempFileDir, 'checksum.txt') + source.text = 'checksum me' + + expect: + SpringIOUtils.computeChecksum(source, 'md5').length() == 32 + } + + void 'computeChecksum rejects an unknown algorithm'() { + given: + File source = new File(tempFileDir, 'checksum.txt') + source.text = 'checksum me' + + when: + SpringIOUtils.computeChecksum(source, 'not-an-algorithm') + + then: + thrown(IllegalArgumentException) + } + + void 'closeQuietly closes without throwing'() { + given: + Closeable closeable = { -> } as Closeable + + expect: + SpringIOUtils.closeQuietly(closeable) + SpringIOUtils.closeQuietly(null) + } + + void 'closeQuietly swallows an IOException from close'() { + given: + Closeable closeable = { -> throw new IOException('boom') } as Closeable + + when: + SpringIOUtils.closeQuietly(closeable) + + then: + noExceptionThrown() + } + + void 'addAll concatenates two arrays preserving order'() { + expect: + SpringIOUtils.addAll(['a', 'b'] as String[], ['c', 'd'] as String[]) == ['a', 'b', 'c', 'd'] + } + } diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/UrlResourceSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/UrlResourceSpec.groovy new file mode 100644 index 00000000000..490372ae693 --- /dev/null +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/UrlResourceSpec.groovy @@ -0,0 +1,109 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package org.grails.io.support + +import spock.lang.Specification + +class UrlResourceSpec extends Specification { + + void 'equals is true for the same instance'() { + given: + UrlResource resource = new UrlResource('file:///tmp/foo.txt') + + expect: + resource == resource + } + + void 'equals is true for two resources with the same cleaned URL'() { + given: + UrlResource one = new UrlResource('file:///tmp/foo.txt') + UrlResource two = new UrlResource('file:///tmp/foo.txt') + + expect: + one == two + one.hashCode() == two.hashCode() + } + + void 'equals is false for resources with different URLs'() { + given: + UrlResource one = new UrlResource('file:///tmp/foo.txt') + UrlResource two = new UrlResource('file:///tmp/bar.txt') + + expect: + one != two + } + + void 'equals is false when compared against an unrelated object'() { + given: + UrlResource resource = new UrlResource('file:///tmp/foo.txt') + + expect: + !resource.equals('not a resource') + } + + void 'equals does not infinitely recurse for a distinct non-equal resource'() { + given: + UrlResource one = new UrlResource('file:///tmp/foo.txt') + UrlResource two = new UrlResource('file:///tmp/bar.txt') + + when: + boolean result = one.equals(two) + + then: + noExceptionThrown() + !result + } + + void 'getURL returns the original URL'() { + given: + UrlResource resource = new UrlResource('file:///tmp/foo.txt') + + expect: + resource.getURL().toString() == 'file:/tmp/foo.txt' + } + + void 'getFilename returns the last path segment'() { + given: + UrlResource resource = new UrlResource('file:///tmp/foo.txt') + + expect: + resource.getFilename() == 'foo.txt' + } + + void 'createRelative resolves a relative path against the base URL'() { + given: + UrlResource resource = new UrlResource('file:///tmp/dir/foo.txt') + + when: + Resource relative = resource.createRelative('bar.txt') + + then: + relative.getURL().toString() == 'file:/tmp/dir/bar.txt' + } + + void 'getDescription includes the URL'() { + given: + UrlResource resource = new UrlResource('file:///tmp/foo.txt') + + expect: + resource.getDescription() == 'URL [file:/tmp/foo.txt]' + resource.toString() == resource.getDescription() + } + +} From a3f34e4a1cb2be62d09aed5a209f08470b4f1802 Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Sat, 10 Oct 2026 12:07:24 -0500 Subject: [PATCH 02/14] refactor: convert grails-common to Groovy Converts the Java sources of grails-common to Groovy. Part of the per-project split requested by the maintainers on #16351; end state taken from the 9.0.x-java-to-groovy branch (origin/9.0.x..e55fd9e0eb). Co-Authored-By: Claude Sonnet 5.5 --- ...sing.java => AheadOfTimeProcessing.groovy} | 14 +- ...ableTypes.java => RegistrableTypes.groovy} | 180 +++++++++--------- ...ctionUtils.java => ReflectionUtils.groovy} | 108 +++++------ .../aot/AheadOfTimeProcessingSpec.groovy | 54 ++++++ 4 files changed, 207 insertions(+), 149 deletions(-) rename grails-common/src/main/groovy/org/apache/grails/common/aot/{AheadOfTimeProcessing.java => AheadOfTimeProcessing.groovy} (87%) rename grails-common/src/main/groovy/org/apache/grails/common/aot/{RegistrableTypes.java => RegistrableTypes.groovy} (57%) rename grails-common/src/main/groovy/org/apache/grails/common/reflect/{ReflectionUtils.java => ReflectionUtils.groovy} (68%) create mode 100644 grails-common/src/test/groovy/org/apache/grails/common/aot/AheadOfTimeProcessingSpec.groovy diff --git a/grails-common/src/main/groovy/org/apache/grails/common/aot/AheadOfTimeProcessing.java b/grails-common/src/main/groovy/org/apache/grails/common/aot/AheadOfTimeProcessing.groovy similarity index 87% rename from grails-common/src/main/groovy/org/apache/grails/common/aot/AheadOfTimeProcessing.java rename to grails-common/src/main/groovy/org/apache/grails/common/aot/AheadOfTimeProcessing.groovy index 29441158908..44b8f8846bf 100644 --- a/grails-common/src/main/groovy/org/apache/grails/common/aot/AheadOfTimeProcessing.java +++ b/grails-common/src/main/groovy/org/apache/grails/common/aot/AheadOfTimeProcessing.groovy @@ -16,10 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.grails.common.aot; +package org.apache.grails.common.aot -import org.springframework.context.aot.AbstractAotProcessor; -import org.springframework.core.SpringProperties; +import groovy.transform.CompileStatic +import org.springframework.context.aot.AbstractAotProcessor +import org.springframework.core.SpringProperties /** * Whether the application's code is being written out rather than run. @@ -39,7 +40,8 @@ * * @since 8.0 */ -public final class AheadOfTimeProcessing { +@CompileStatic +final class AheadOfTimeProcessing { private AheadOfTimeProcessing() { } @@ -47,8 +49,8 @@ private AheadOfTimeProcessing() { /** * @return whether this is running inside ahead-of-time processing rather than in an application */ - public static boolean isGeneratingCode() { - return SpringProperties.getFlag(AbstractAotProcessor.AOT_PROCESSING); + static boolean isGeneratingCode() { + return SpringProperties.getFlag(AbstractAotProcessor.AOT_PROCESSING) } } diff --git a/grails-common/src/main/groovy/org/apache/grails/common/aot/RegistrableTypes.java b/grails-common/src/main/groovy/org/apache/grails/common/aot/RegistrableTypes.groovy similarity index 57% rename from grails-common/src/main/groovy/org/apache/grails/common/aot/RegistrableTypes.java rename to grails-common/src/main/groovy/org/apache/grails/common/aot/RegistrableTypes.groovy index e32f18e852f..106539a5bff 100644 --- a/grails-common/src/main/groovy/org/apache/grails/common/aot/RegistrableTypes.java +++ b/grails-common/src/main/groovy/org/apache/grails/common/aot/RegistrableTypes.groovy @@ -16,25 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.grails.common.aot; - -import java.io.IOException; -import java.io.InputStream; -import java.util.LinkedHashSet; -import java.util.Set; - -import org.jspecify.annotations.Nullable; - -import org.springframework.asm.ClassReader; -import org.springframework.asm.ClassVisitor; -import org.springframework.asm.ConstantDynamic; -import org.springframework.asm.FieldVisitor; -import org.springframework.asm.Handle; -import org.springframework.asm.Label; -import org.springframework.asm.MethodVisitor; -import org.springframework.asm.SpringAsmInfo; -import org.springframework.asm.Type; -import org.springframework.util.ClassUtils; +package org.apache.grails.common.aot + +import groovy.transform.CompileStatic +import org.jspecify.annotations.Nullable +import org.springframework.asm.ClassReader +import org.springframework.asm.ClassVisitor +import org.springframework.asm.ConstantDynamic +import org.springframework.asm.FieldVisitor +import org.springframework.asm.Handle +import org.springframework.asm.Label +import org.springframework.asm.MethodVisitor +import org.springframework.asm.SpringAsmInfo +import org.springframework.asm.Type +import org.springframework.util.ClassUtils /** * Decides whether a type found by scanning can be registered for reflection. @@ -53,7 +48,8 @@ * * @since 8.0 */ -public final class RegistrableTypes { +@CompileStatic +final class RegistrableTypes { private RegistrableTypes() { } @@ -65,12 +61,12 @@ private RegistrableTypes() { * @param classLoader the loader to resolve against * @return whether the type can be registered */ - public static boolean loads(String className, @Nullable ClassLoader classLoader) { - int declaring = className.indexOf('$'); + static boolean loads(String className, @Nullable ClassLoader classLoader) { + int declaring = className.indexOf('$') if (declaring > 0 && !ClassUtils.isPresent(className.substring(0, declaring), classLoader)) { - return false; + return false } - return ClassUtils.isPresent(className, classLoader); + return ClassUtils.isPresent(className, classLoader) } /** @@ -81,171 +77,175 @@ public static boolean loads(String className, @Nullable ClassLoader classLoader) * @param classLoader the loader to resolve against * @return whether the type can be registered */ - public static boolean referencesLoad(InputStream bytecode, @Nullable ClassLoader classLoader) { - Set referenced = new LinkedHashSet<>(); + static boolean referencesLoad(InputStream bytecode, @Nullable ClassLoader classLoader) { + Set referenced = new LinkedHashSet<>() try (InputStream input = bytecode) { new ClassReader(input).accept(new ReferenceCollector(referenced), - ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); + ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES) } catch (IOException | RuntimeException ex) { - return false; + return false } - for (String name : referenced) { + for (String name in referenced) { if (!ClassUtils.isPresent(name, classLoader)) { - return false; + return false } } - return true; + return true } /** Collects the types a class names, in its own declaration and in the bodies of its methods. */ private static final class ReferenceCollector extends ClassVisitor { - private final Set referenced; + private final Set referenced private ReferenceCollector(Set referenced) { - super(SpringAsmInfo.ASM_VERSION); - this.referenced = referenced; + super(SpringAsmInfo.ASM_VERSION) + this.referenced = referenced } private void add(@Nullable String internalName) { if (internalName == null) { - return; + return } - if (internalName.startsWith("[")) { + if (internalName.startsWith('[')) { // an array names its element type, which is the class that has to be there - addType(Type.getType(internalName)); - return; + addType(Type.getType(internalName)) + return } - String className = ClassUtils.convertResourcePathToClassName(internalName); + String className = ClassUtils.convertResourcePathToClassName(internalName) // the JDK is always present, and skipping it keeps this to the classes that can be absent - if (!className.startsWith("java.") && !className.startsWith("jdk.")) { - referenced.add(className); + if (!className.startsWith('java.') && !className.startsWith('jdk.')) { + referenced.add(className) } } /** Adds a type, reducing an array to the element type it is an array of. */ private void addType(@Nullable Type type) { if (type == null) { - return; + return } - Type element = type; + Type element = type while (element.getSort() == Type.ARRAY) { - element = element.getElementType(); + element = element.getElementType() } if (element.getSort() == Type.OBJECT) { - add(element.getInternalName()); + add(element.getInternalName()) } } /** Adds every type a method descriptor names: what it takes and what it gives back. */ private void addMethodDescriptor(@Nullable String descriptor) { if (descriptor == null) { - return; + return } - for (Type argument : Type.getArgumentTypes(descriptor)) { - addType(argument); + for (Type argument in Type.getArgumentTypes(descriptor)) { + addType(argument) } - addType(Type.getReturnType(descriptor)); + addType(Type.getReturnType(descriptor)) } /** Adds whichever constant carries a type: a class literal, a handle, or a method type. */ private void addConstant(@Nullable Object constant) { - if (constant instanceof Type type) { + if (constant instanceof Type) { + Type type = (Type) constant if (type.getSort() == Type.METHOD) { - addMethodDescriptor(type.getDescriptor()); + addMethodDescriptor(type.getDescriptor()) } else { - addType(type); + addType(type) } } - else if (constant instanceof Handle handle) { - add(handle.getOwner()); - addMethodDescriptor(handle.getDesc()); + else if (constant instanceof Handle) { + Handle handle = (Handle) constant + add(handle.getOwner()) + addMethodDescriptor(handle.getDesc()) } - else if (constant instanceof ConstantDynamic dynamic) { - addType(Type.getType(dynamic.getDescriptor())); - add(dynamic.getBootstrapMethod().getOwner()); + else if (constant instanceof ConstantDynamic) { + ConstantDynamic dynamic = (ConstantDynamic) constant + addType(Type.getType(dynamic.getDescriptor())) + add(dynamic.getBootstrapMethod().getOwner()) for (int i = 0; i < dynamic.getBootstrapMethodArgumentCount(); i++) { - addConstant(dynamic.getBootstrapMethodArgument(i)); + addConstant(dynamic.getBootstrapMethodArgument(i)) } } } @Override - public void visit(int version, int access, String name, String signature, String superName, + void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { - add(superName); + add(superName) if (interfaces != null) { - for (String each : interfaces) { - add(each); + for (String each in interfaces) { + add(each) } } } @Override - public FieldVisitor visitField(int access, String name, String descriptor, String signature, + FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { - addType(Type.getType(descriptor)); - return null; + addType(Type.getType(descriptor)) + return null } @Override - public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, + MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { - addMethodDescriptor(descriptor); + addMethodDescriptor(descriptor) if (exceptions != null) { - for (String each : exceptions) { - add(each); + for (String each in exceptions) { + add(each) } } return new MethodVisitor(SpringAsmInfo.ASM_VERSION) { @Override - public void visitTypeInsn(int opcode, String type) { - add(type); + void visitTypeInsn(int opcode, String type) { + add(type) } @Override - public void visitMethodInsn(int opcode, String owner, String methodName, + void visitMethodInsn(int opcode, String owner, String methodName, String methodDescriptor, boolean isInterface) { - add(owner); - addMethodDescriptor(methodDescriptor); + add(owner) + addMethodDescriptor(methodDescriptor) } @Override - public void visitFieldInsn(int opcode, String owner, String fieldName, + void visitFieldInsn(int opcode, String owner, String fieldName, String fieldDescriptor) { - add(owner); - addType(Type.getType(fieldDescriptor)); + add(owner) + addType(Type.getType(fieldDescriptor)) } @Override - public void visitLdcInsn(Object value) { + void visitLdcInsn(Object value) { // a class literal, which names a type without ever calling anything on it - addConstant(value); + addConstant(value) } @Override - public void visitInvokeDynamicInsn(String methodName, String methodDescriptor, + void visitInvokeDynamicInsn(String methodName, String methodDescriptor, Handle bootstrap, Object... arguments) { // how a Groovy call site names what it dispatches on - addMethodDescriptor(methodDescriptor); - addConstant(bootstrap); - for (Object argument : arguments) { - addConstant(argument); + addMethodDescriptor(methodDescriptor) + addConstant(bootstrap) + for (Object argument in arguments) { + addConstant(argument) } } @Override - public void visitMultiANewArrayInsn(String descriptor, int dimensions) { - addType(Type.getType(descriptor)); + void visitMultiANewArrayInsn(String arrayDescriptor, int dimensions) { + addType(Type.getType(arrayDescriptor)) } @Override - public void visitTryCatchBlock(Label start, Label end, Label handler, String type) { - add(type); + void visitTryCatchBlock(Label start, Label end, Label handler, String type) { + add(type) } - }; + } } } + } diff --git a/grails-common/src/main/groovy/org/apache/grails/common/reflect/ReflectionUtils.java b/grails-common/src/main/groovy/org/apache/grails/common/reflect/ReflectionUtils.groovy similarity index 68% rename from grails-common/src/main/groovy/org/apache/grails/common/reflect/ReflectionUtils.java rename to grails-common/src/main/groovy/org/apache/grails/common/reflect/ReflectionUtils.groovy index ec0c17f0498..e067912c4dd 100644 --- a/grails-common/src/main/groovy/org/apache/grails/common/reflect/ReflectionUtils.java +++ b/grails-common/src/main/groovy/org/apache/grails/common/reflect/ReflectionUtils.groovy @@ -16,20 +16,20 @@ * specific language governing permissions and limitations * under the License. */ -package org.apache.grails.common.reflect; +package org.apache.grails.common.reflect -import java.lang.reflect.Field; -import java.lang.reflect.InaccessibleObjectException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; +import java.lang.reflect.Field +import java.lang.reflect.InaccessibleObjectException +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.util.concurrent.ConcurrentHashMap -import org.jspecify.annotations.Nullable; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; +import groovy.transform.CompileStatic +import org.jspecify.annotations.Nullable +import org.slf4j.Logger +import org.slf4j.LoggerFactory -import org.springframework.util.ClassUtils; +import org.springframework.util.ClassUtils /** * Reads the properties of a bean whose class is not {@code public}. @@ -45,19 +45,20 @@ * * @since 8.0.0 */ -public final class ReflectionUtils { +@CompileStatic +final class ReflectionUtils { - private static final Logger LOG = LoggerFactory.getLogger(ReflectionUtils.class); + private static final Logger LOG = LoggerFactory.getLogger(ReflectionUtils) /** Best-effort upper bound, so that generated class names cannot grow the set without limit. */ - private static final int MAX_WARNED_CLASSES = 1024; + private static final int MAX_WARNED_CLASSES = 1024 /** * Keyed by class name rather than by {@link Class}, so that reporting a class never retains its * class loader. A reload therefore does not report the same class again, which is intended: a * reload loop would otherwise repeat every warning. */ - private static final Set WARNED_NON_PUBLIC_CLASSES = ConcurrentHashMap.newKeySet(); + private static final Set WARNED_NON_PUBLIC_CLASSES = ConcurrentHashMap.newKeySet() /** * Packages whose classes an application cannot declare public itself, so reporting them would @@ -67,10 +68,10 @@ public final class ReflectionUtils { *

The {@code grails} packages are deliberately absent: a framework class reaching this code * is worth seeing, and the tests declare their fixtures under {@code org.grails}. */ - private static final String[] NON_APPLICATION_PACKAGES = { - "java.", "javax.", "jakarta.", "groovy.", "org.apache.groovy.", "org.codehaus.groovy.", - "org.springframework." - }; + private static final String[] NON_APPLICATION_PACKAGES = [ + 'java.', 'javax.', 'jakarta.', 'groovy.', 'org.apache.groovy.', 'org.codehaus.groovy.', + 'org.springframework.' + ] as String[] private ReflectionUtils() { } @@ -89,17 +90,17 @@ private ReflectionUtils() { * @param target the instance being read, or {@code null} for a static read method * @return a method that may be invoked on {@code target} */ - public static Method resolveInvokableReadMethod(Method readMethod, Class targetClass, @Nullable Object target) + static Method resolveInvokableReadMethod(Method readMethod, Class targetClass, @Nullable Object target) throws NoSuchMethodException { - Method invokable = ClassUtils.getPubliclyAccessibleMethodIfPossible(readMethod, targetClass); + Method invokable = ClassUtils.getPubliclyAccessibleMethodIfPossible(readMethod, targetClass) if (canAccess(invokable, target)) { - return invokable; + return invokable } // getDeclaredMethod cannot fail here: invokable was resolved from this very class's methods. - Method widened = invokable.getDeclaringClass() - .getDeclaredMethod(invokable.getName(), invokable.getParameterTypes()); - widened.setAccessible(true); - return widened; + Method widened = invokable.declaringClass + .getDeclaredMethod(invokable.name, invokable.parameterTypes) + widened.setAccessible(true) + return widened } /** @@ -113,21 +114,21 @@ public static Method resolveInvokableReadMethod(Method readMethod, Class targ * @return {@code false} when the field cannot be read, in which case the caller should skip it * rather than fail the whole read */ - public static boolean tryMakeReadable(Field field, @Nullable Object target) { + static boolean tryMakeReadable(Field field, @Nullable Object target) { if (canAccess(field, target)) { - return true; + return true } try { - field.setAccessible(true); - return true; + field.setAccessible(true) + return true } catch (InaccessibleObjectException | SecurityException e) { // The declaring class is in a named module that does not open its package to us if (LOG.isDebugEnabled()) { - LOG.debug("Cannot read field [{}] of [{}]: {}", - field.getName(), field.getDeclaringClass().getName(), e.getMessage()); + LOG.debug('Cannot read field [{}] of [{}]: {}', + field.name, field.declaringClass.name, e.message) } - return false; + return false } } @@ -142,31 +143,31 @@ public static boolean tryMakeReadable(Field field, @Nullable Object target) { * * @return whether this call was the one that reported the class */ - public static boolean warnOnNonPublicClass(@Nullable Class clazz) { - if (clazz == null || Modifier.isPublic(clazz.getModifiers()) || !isApplicationClass(clazz)) { - return false; + static boolean warnOnNonPublicClass(@Nullable Class clazz) { + if (clazz == null || Modifier.isPublic(clazz.modifiers) || !isApplicationClass(clazz)) { + return false } if (WARNED_NON_PUBLIC_CLASSES.size() >= MAX_WARNED_CLASSES || - !WARNED_NON_PUBLIC_CLASSES.add(clazz.getName())) { - return false; + !WARNED_NON_PUBLIC_CLASSES.add(clazz.name)) { + return false } if (LOG.isWarnEnabled()) { - LOG.warn("Class [{}] is not public. Grails reads its properties through compatibility handling that may " + - "be withdrawn in a future major release. Declare it as a named public class so that it " + - "reads as a standard JavaBean, or register an ObjectMarshaller for it if the class is " + - "not yours. To silence this, set the log level of [{}] above WARN. " + - "(warned once per class)", - clazz.getName(), LOG.getName()); + LOG.warn('Class [{}] is not public. Grails reads its properties through compatibility handling that may ' + + 'be withdrawn in a future major release. Declare it as a named public class so that it ' + + 'reads as a standard JavaBean, or register an ObjectMarshaller for it if the class is ' + + 'not yours. To silence this, set the log level of [{}] above WARN. ' + + '(warned once per class)', + clazz.name, LOG.name) } - return true; + return true } /** * Forgets which classes have been reported. Intended for tests, which would otherwise depend on * whether another test in the same JVM reported the same class first. */ - public static void resetWarnedClasses() { - WARNED_NON_PUBLIC_CLASSES.clear(); + static void resetWarnedClasses() { + WARNED_NON_PUBLIC_CLASSES.clear() } /** @@ -174,23 +175,24 @@ public static void resetWarnedClasses() { * property descriptors or declared fields should not have to know. */ private static boolean canAccess(Method method, @Nullable Object target) { - return method.canAccess(Modifier.isStatic(method.getModifiers()) ? null : target); + return method.canAccess(Modifier.isStatic(method.modifiers) ? null : target) } private static boolean canAccess(Field field, @Nullable Object target) { - return field.canAccess(Modifier.isStatic(field.getModifiers()) ? null : target); + return field.canAccess(Modifier.isStatic(field.modifiers) ? null : target) } private static boolean isApplicationClass(Class clazz) { if (clazz.isSynthetic()) { - return false; + return false } - String name = clazz.getName(); + String name = clazz.name for (String prefix : NON_APPLICATION_PACKAGES) { if (name.startsWith(prefix)) { - return false; + return false } } - return true; + return true } + } diff --git a/grails-common/src/test/groovy/org/apache/grails/common/aot/AheadOfTimeProcessingSpec.groovy b/grails-common/src/test/groovy/org/apache/grails/common/aot/AheadOfTimeProcessingSpec.groovy new file mode 100644 index 00000000000..4e7a05dd9a1 --- /dev/null +++ b/grails-common/src/test/groovy/org/apache/grails/common/aot/AheadOfTimeProcessingSpec.groovy @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package org.apache.grails.common.aot + +import org.springframework.context.aot.AbstractAotProcessor +import org.springframework.core.SpringProperties +import spock.lang.Specification + +class AheadOfTimeProcessingSpec extends Specification { + + void cleanup() { + SpringProperties.setProperty(AbstractAotProcessor.AOT_PROCESSING, null) + } + + void 'code generation is reported only while spring sets its processing flag'() { + expect: + !AheadOfTimeProcessing.isGeneratingCode() + + when: + SpringProperties.setFlag(AbstractAotProcessor.AOT_PROCESSING) + + then: + AheadOfTimeProcessing.isGeneratingCode() + + when: + SpringProperties.setProperty(AbstractAotProcessor.AOT_PROCESSING, 'false') + + then: + !AheadOfTimeProcessing.isGeneratingCode() + } + + void 'the helper is a static utility'() { + expect: + java.lang.reflect.Modifier.isFinal(AheadOfTimeProcessing.modifiers) + AheadOfTimeProcessing.declaredConstructors.every { java.lang.reflect.Modifier.isPrivate(it.modifiers) } + } + +} From 46244a713b4ff30d8b759ec492b18541ee87dc38 Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Sat, 10 Oct 2026 12:07:24 -0500 Subject: [PATCH 03/14] refactor: convert grails-beans-dsl to Groovy Converts the Java sources of grails-beans-dsl to Groovy. Part of the per-project split requested by the maintainers on #16351; end state taken from the 9.0.x-java-to-groovy branch (origin/9.0.x..e55fd9e0eb). Co-Authored-By: Claude Sonnet 5.5 --- .../beans/ConditionalOnGrailsEnv.groovy} | 24 +- .../grails/compiler/beans/GrailsBeans.groovy} | 22 +- .../AutoConfigurationImportsWriter.groovy} | 157 +- .../GrailsBeansASTTransformation.groovy} | 2715 ++++++++--------- .../beans/OnGrailsEnvCondition.groovy} | 67 +- 5 files changed, 1483 insertions(+), 1502 deletions(-) rename grails-beans-dsl/src/main/{java/grails/compiler/beans/ConditionalOnGrailsEnv.java => groovy/grails/compiler/beans/ConditionalOnGrailsEnv.groovy} (79%) rename grails-beans-dsl/src/main/{java/grails/compiler/beans/GrailsBeans.java => groovy/grails/compiler/beans/GrailsBeans.groovy} (98%) rename grails-beans-dsl/src/main/{java/org/grails/compiler/beans/AutoConfigurationImportsWriter.java => groovy/org/grails/compiler/beans/AutoConfigurationImportsWriter.groovy} (71%) rename grails-beans-dsl/src/main/{java/org/grails/compiler/beans/GrailsBeansASTTransformation.java => groovy/org/grails/compiler/beans/GrailsBeansASTTransformation.groovy} (67%) rename grails-beans-dsl/src/main/{java/org/grails/compiler/beans/OnGrailsEnvCondition.java => groovy/org/grails/compiler/beans/OnGrailsEnvCondition.groovy} (71%) diff --git a/grails-beans-dsl/src/main/java/grails/compiler/beans/ConditionalOnGrailsEnv.java b/grails-beans-dsl/src/main/groovy/grails/compiler/beans/ConditionalOnGrailsEnv.groovy similarity index 79% rename from grails-beans-dsl/src/main/java/grails/compiler/beans/ConditionalOnGrailsEnv.java rename to grails-beans-dsl/src/main/groovy/grails/compiler/beans/ConditionalOnGrailsEnv.groovy index 8c8a39fa106..9a37ed8874f 100644 --- a/grails-beans-dsl/src/main/java/grails/compiler/beans/ConditionalOnGrailsEnv.java +++ b/grails-beans-dsl/src/main/groovy/grails/compiler/beans/ConditionalOnGrailsEnv.groovy @@ -16,17 +16,17 @@ * specific language governing permissions and limitations * under the License. */ -package grails.compiler.beans; +package grails.compiler.beans -import java.lang.annotation.Documented; -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import java.lang.annotation.Documented +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target -import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Conditional -import org.grails.compiler.beans.OnGrailsEnvCondition; +import org.grails.compiler.beans.OnGrailsEnvCondition /** * Registers the annotated bean or configuration only in the named Grails environments. @@ -41,16 +41,16 @@ * @see OnGrailsEnvCondition */ @Retention(RetentionPolicy.RUNTIME) -@Target({ElementType.TYPE, ElementType.METHOD}) +@Target([ElementType.TYPE, ElementType.METHOD]) @Documented -@Conditional(OnGrailsEnvCondition.class) -public @interface ConditionalOnGrailsEnv { +@Conditional(OnGrailsEnvCondition) +@interface ConditionalOnGrailsEnv { /** * The environment names to match, as {@code grails.util.Environment} reports them - the * built-in ones lowercased ({@code development}, {@code production}, {@code test}, * {@code application}), or a custom environment's own name. Matching any one is enough. */ - String[] value(); + String[] value() } diff --git a/grails-beans-dsl/src/main/java/grails/compiler/beans/GrailsBeans.java b/grails-beans-dsl/src/main/groovy/grails/compiler/beans/GrailsBeans.groovy similarity index 98% rename from grails-beans-dsl/src/main/java/grails/compiler/beans/GrailsBeans.java rename to grails-beans-dsl/src/main/groovy/grails/compiler/beans/GrailsBeans.groovy index f60d5af026b..2a3f0053522 100644 --- a/grails-beans-dsl/src/main/java/grails/compiler/beans/GrailsBeans.java +++ b/grails-beans-dsl/src/main/groovy/grails/compiler/beans/GrailsBeans.groovy @@ -16,14 +16,14 @@ * specific language governing permissions and limitations * under the License. */ -package grails.compiler.beans; +package grails.compiler.beans -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; +import java.lang.annotation.ElementType +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy +import java.lang.annotation.Target -import org.codehaus.groovy.transform.GroovyASTTransformationClass; +import org.codehaus.groovy.transform.GroovyASTTransformationClass /** * Marks a class whose {@code beans} closure property is a bean-definition DSL that should be @@ -330,14 +330,14 @@ */ @Retention(RetentionPolicy.CLASS) @Target(ElementType.TYPE) -@GroovyASTTransformationClass("org.grails.compiler.beans.GrailsBeansASTTransformation") -public @interface GrailsBeans { +@GroovyASTTransformationClass('org.grails.compiler.beans.GrailsBeansASTTransformation') +@interface GrailsBeans { /** * The simple name of the static nested class a unit test's {@code beans} block compiles onto (see * Unit tests above), which the testing support looks for among a test's nested classes. */ - String UNIT_TEST_CONFIGURATION_NAME = "BeansConfiguration"; + String UNIT_TEST_CONFIGURATION_NAME = 'BeansConfiguration' /** * The name of the generated sibling class, for a {@code grails.plugins.Plugin} subclass. The @@ -358,7 +358,7 @@ * not otherwise own splits that package across two jars, which a modular or native-image * consumer pays for, so name one of the plugin's own. */ - String autoConfigurationName() default ""; + String autoConfigurationName() default '' /** * Additional annotation types to move from a {@code grails.plugins.Plugin} subclass onto its @@ -369,6 +369,6 @@ * plugin class, where Spring never sees it. E.g. * {@code @GrailsBeans(moveAnnotations = [SomeVendorAnnotation])}. */ - Class[] moveAnnotations() default {}; + Class[] moveAnnotations() default [] } diff --git a/grails-beans-dsl/src/main/java/org/grails/compiler/beans/AutoConfigurationImportsWriter.java b/grails-beans-dsl/src/main/groovy/org/grails/compiler/beans/AutoConfigurationImportsWriter.groovy similarity index 71% rename from grails-beans-dsl/src/main/java/org/grails/compiler/beans/AutoConfigurationImportsWriter.java rename to grails-beans-dsl/src/main/groovy/org/grails/compiler/beans/AutoConfigurationImportsWriter.groovy index 4ab3d48daa9..f390088079c 100644 --- a/grails-beans-dsl/src/main/java/org/grails/compiler/beans/AutoConfigurationImportsWriter.java +++ b/grails-beans-dsl/src/main/groovy/org/grails/compiler/beans/AutoConfigurationImportsWriter.groovy @@ -16,21 +16,15 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.compiler.beans; +package org.grails.compiler.beans -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.util.Collections; -import java.util.Map; -import java.util.Set; -import java.util.TreeSet; -import java.util.WeakHashMap; +import java.nio.charset.StandardCharsets +import java.nio.file.Files -import org.codehaus.groovy.control.SourceUnit; -import org.codehaus.groovy.control.messages.WarningMessage; -import org.codehaus.groovy.syntax.SyntaxException; +import groovy.transform.CompileStatic +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.control.messages.WarningMessage +import org.codehaus.groovy.syntax.SyntaxException /** * Registers a generated auto-configuration in @@ -62,19 +56,20 @@ * * @since 8.0 */ -public final class AutoConfigurationImportsWriter { +@CompileStatic +final class AutoConfigurationImportsWriter { public static final String IMPORTS_LOCATION = - "META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports"; + 'META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports' - static final String SOURCE_IMPORTS_LOCATION = "src/main/resources/" + IMPORTS_LOCATION; + static final String SOURCE_IMPORTS_LOCATION = 'src/main/resources/' + IMPORTS_LOCATION /** Set by the Grails Gradle plugin on the compiler's fork options; see GrailsAppBaseDirProvider. */ - private static final String BASE_DIR_PROPERTY = "base.dir"; + private static final String BASE_DIR_PROPERTY = 'base.dir' - private static final String COMMENT_START = "#"; + private static final String COMMENT_START = '#' - private static final String CLASS_FILE_EXTENSION = ".class"; + private static final String CLASS_FILE_EXTENSION = '.class' /** * What each compilation has registered so far, so an entry survives the pruning below before the @@ -83,7 +78,7 @@ public final class AutoConfigurationImportsWriter { * compilation, which is what makes the state per-build rather than per-JVM in a reused daemon. */ private static final Map> REGISTERED_BY_COMPILATION = - Collections.synchronizedMap(new WeakHashMap<>()); + Collections.synchronizedMap(new WeakHashMap<>()) private AutoConfigurationImportsWriter() { } @@ -101,47 +96,47 @@ private AutoConfigurationImportsWriter() { * @param compilation what scopes names registered before their class files are written * @return {@code true} when the file was written */ - public static boolean register(String className, File targetDirectory, SourceUnit source, Object compilation) { + static boolean register(String className, File targetDirectory, SourceUnit source, Object compilation) { if (className == null || className.isEmpty() || targetDirectory == null) { - return false; + return false } - File sourceDirectory = findSourceDirectory(targetDirectory); - File handAuthored = sourceDirectory == null ? null : new File(sourceDirectory, SOURCE_IMPORTS_LOCATION); + File sourceDirectory = findSourceDirectory(targetDirectory) + File handAuthored = sourceDirectory == null ? null : new File(sourceDirectory, SOURCE_IMPORTS_LOCATION) if (handAuthored != null && handAuthored.isFile()) { - Set handAuthoredEntries = new TreeSet<>(); - readEntries(handAuthored, handAuthoredEntries); + Set handAuthoredEntries = new TreeSet<>() + readEntries(handAuthored, handAuthoredEntries) if (!handAuthoredEntries.contains(className)) { - warn(source, className + " is generated from a beans closure but is not listed in " + - SOURCE_IMPORTS_LOCATION + ", so Spring Boot will not read it. Add it there, or delete " + - "that file once it holds nothing that is not generated and it will be written for you."); + warn(source, className + ' is generated from a beans closure but is not listed in ' + + SOURCE_IMPORTS_LOCATION + ', so Spring Boot will not read it. Add it there, or delete ' + + 'that file once it holds nothing that is not generated and it will be written for you.') } - write(new File(targetDirectory, IMPORTS_LOCATION), Collections.emptySet(), source); - return false; + write(new File(targetDirectory, IMPORTS_LOCATION), Collections.emptySet(), source) + return false } - Set registeredHere = registeredBy(compilation); - registeredHere.add(className); + Set registeredHere = registeredBy(compilation) + registeredHere.add(className) - File importsFile = new File(targetDirectory, IMPORTS_LOCATION); - Set onDisk = new TreeSet<>(); - readEntries(importsFile, onDisk); + File importsFile = new File(targetDirectory, IMPORTS_LOCATION) + Set onDisk = new TreeSet<>() + readEntries(importsFile, onDisk) // A descriptor that was renamed, deleted, or given a different autoConfigurationName leaves // an entry naming a class that is no longer generated, and Spring Boot fails to start on an // auto-configuration it cannot load. Anything this compilation registered is kept regardless: // its class file is written in a later phase than this one runs in. - Set entries = new TreeSet<>(onDisk); - entries.removeIf(entry -> !registeredHere.contains(entry) && !isGeneratedHere(targetDirectory, entry)); - entries.addAll(registeredHere); + Set entries = new TreeSet<>(onDisk) + entries.removeIf({ entry -> !registeredHere.contains(entry) && !isGeneratedHere(targetDirectory, entry) }) + entries.addAll(registeredHere) // Against what is on disk rather than against the pruned set: a call that only drops a stale // entry adds nothing, and comparing the two would leave the entry it just decided to drop. if (entries.equals(onDisk) && importsFile.isFile()) { - return false; + return false } - return write(importsFile, entries, source); + return write(importsFile, entries, source) } /** @@ -152,23 +147,23 @@ public static boolean register(String className, File targetDirectory, SourceUni private static boolean write(File importsFile, Set entries, SourceUnit source) { try { if (entries.isEmpty()) { - return Files.deleteIfExists(importsFile.toPath()); + return Files.deleteIfExists(importsFile.toPath()) } - Files.createDirectories(importsFile.toPath().getParent()); + Files.createDirectories(importsFile.toPath().getParent()) // Sorted and newline-terminated, so recompiling the same sources rewrites the same bytes. - Files.write(importsFile.toPath(), (String.join("\n", entries) + "\n") - .getBytes(StandardCharsets.UTF_8)); - return true; + Files.write(importsFile.toPath(), (String.join('\n', entries) + '\n') + .getBytes(StandardCharsets.UTF_8)) + return true } catch (IOException notWritable) { - String message = "Could not write generated auto-configuration imports at " + importsFile; + String message = 'Could not write generated auto-configuration imports at ' + importsFile if (source != null) { - source.addErrorAndContinue(new SyntaxException(message, 1, 1)); + source.addErrorAndContinue(new SyntaxException(message, 1, 1)) } else { - throw new IllegalStateException(message, notWritable); + throw new IllegalStateException(message, notWritable) } - return false; + return false } } @@ -187,54 +182,54 @@ private static boolean write(File importsFile, Set entries, SourceUnit s * @param source the source being compiled, used to report a metadata write failure * @return {@code true} when an entry was dropped */ - public static boolean reconcile(File targetDirectory, Object compilation, SourceUnit source) { + static boolean reconcile(File targetDirectory, Object compilation, SourceUnit source) { if (targetDirectory == null) { - return false; + return false } - File importsFile = new File(targetDirectory, IMPORTS_LOCATION); - File sourceDirectory = findSourceDirectory(targetDirectory); - File handAuthored = sourceDirectory == null ? null : new File(sourceDirectory, SOURCE_IMPORTS_LOCATION); + File importsFile = new File(targetDirectory, IMPORTS_LOCATION) + File sourceDirectory = findSourceDirectory(targetDirectory) + File handAuthored = sourceDirectory == null ? null : new File(sourceDirectory, SOURCE_IMPORTS_LOCATION) if (handAuthored != null && handAuthored.isFile()) { - return write(importsFile, Collections.emptySet(), source); + return write(importsFile, Collections.emptySet(), source) } if (!importsFile.isFile()) { // Nothing generated here, which is also how a module keeping the file by hand looks. - return false; + return false } - Set entries = new TreeSet<>(); - readEntries(importsFile, entries); - Set registeredHere = registeredBy(compilation); - Set kept = new TreeSet<>(); - for (String entry : entries) { + Set entries = new TreeSet<>() + readEntries(importsFile, entries) + Set registeredHere = registeredBy(compilation) + Set kept = new TreeSet<>() + for (String entry in entries) { if (registeredHere.contains(entry) || isGeneratedHere(targetDirectory, entry)) { - kept.add(entry); + kept.add(entry) } } if (kept.equals(entries)) { - return false; + return false } - return write(importsFile, kept, source); + return write(importsFile, kept, source) } private static Set registeredBy(Object compilation) { if (compilation == null) { // No compilation to scope to, so nothing is remembered between calls; the pruning below // then rests entirely on which class files are present, which is right for a single one. - return Collections.synchronizedSet(new TreeSet<>()); + return Collections.synchronizedSet(new TreeSet<>()) } return REGISTERED_BY_COMPILATION.computeIfAbsent(compilation, - key -> Collections.synchronizedSet(new TreeSet<>())); + { key -> Collections.synchronizedSet(new TreeSet<>()) }) } /** Whether {@code className} is still a class this module generates into its own output. */ private static boolean isGeneratedHere(File targetDirectory, String className) { - return new File(targetDirectory, className.replace('.', File.separatorChar) + CLASS_FILE_EXTENSION).isFile(); + return new File(targetDirectory, className.replace('.'.charAt(0), File.separatorChar) + CLASS_FILE_EXTENSION).isFile() } private static void warn(SourceUnit source, String message) { if (source != null) { - source.getErrorCollector().addWarning(WarningMessage.LIKELY_ERRORS, message, null, source); + source.getErrorCollector().addWarning(WarningMessage.LIKELY_ERRORS, message, null, source) } } @@ -244,34 +239,34 @@ private static void warn(SourceUnit source, String message) { * one, otherwise the directory above the output root. */ private static File findSourceDirectory(File targetDirectory) { - String baseDir = System.getProperty(BASE_DIR_PROPERTY); + String baseDir = System.getProperty(BASE_DIR_PROPERTY) if (baseDir != null && !baseDir.isEmpty()) { - File candidate = new File(baseDir); + File candidate = new File(baseDir) if (candidate.isDirectory()) { - return candidate; + return candidate } } - File directory = targetDirectory; - while (directory != null && !("build".equals(directory.getName()) || "target".equals(directory.getName()))) { - directory = directory.getParentFile(); + File directory = targetDirectory + while (directory != null && !('build'.equals(directory.getName()) || 'target'.equals(directory.getName()))) { + directory = directory.getParentFile() } - return directory == null ? null : directory.getParentFile(); + return directory == null ? null : directory.getParentFile() } /** Adds the names in {@code file}, skipping blanks and the {@code #} comments Spring Boot skips. */ private static void readEntries(File file, Set entries) { if (file == null || !file.isFile()) { - return; + return } try { - for (String line : Files.readAllLines(file.toPath(), StandardCharsets.UTF_8)) { - String entry = line.trim(); + for (String line in Files.readAllLines(file.toPath(), StandardCharsets.UTF_8)) { + String entry = line.trim() if (!entry.isEmpty() && !entry.startsWith(COMMENT_START)) { - entries.add(entry); + entries.add(entry) } } } - catch (IOException unreadable) { + catch (IOException ignored) { // Nothing to merge that can be read; the generated entry is still written below. } } diff --git a/grails-beans-dsl/src/main/java/org/grails/compiler/beans/GrailsBeansASTTransformation.java b/grails-beans-dsl/src/main/groovy/org/grails/compiler/beans/GrailsBeansASTTransformation.groovy similarity index 67% rename from grails-beans-dsl/src/main/java/org/grails/compiler/beans/GrailsBeansASTTransformation.java rename to grails-beans-dsl/src/main/groovy/org/grails/compiler/beans/GrailsBeansASTTransformation.groovy index 9e4bea43302..85955aeeb22 100644 --- a/grails-beans-dsl/src/main/java/org/grails/compiler/beans/GrailsBeansASTTransformation.java +++ b/grails-beans-dsl/src/main/groovy/org/grails/compiler/beans/GrailsBeansASTTransformation.groovy @@ -16,114 +16,100 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.compiler.beans; - -import java.beans.Introspector; -import java.io.File; -import java.io.IOException; -import java.lang.reflect.Method; -import java.lang.reflect.Modifier; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.nio.file.Paths; -import java.util.ArrayList; -import java.util.Collections; -import java.util.HashMap; -import java.util.HashSet; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.stream.Collectors; -import java.util.stream.Stream; - -import javax.lang.model.SourceVersion; - -import groovy.transform.CompilationUnitAware; -import groovy.transform.CompileStatic; -import groovy.transform.TypeChecked; -import groovy.transform.TypeCheckingMode; -import org.apache.groovy.util.BeanUtils; -import org.codehaus.groovy.GroovyBugError; -import org.codehaus.groovy.ast.ASTNode; -import org.codehaus.groovy.ast.AnnotatedNode; -import org.codehaus.groovy.ast.AnnotationNode; -import org.codehaus.groovy.ast.AstToTextHelper; -import org.codehaus.groovy.ast.ClassHelper; -import org.codehaus.groovy.ast.ClassNode; -import org.codehaus.groovy.ast.CodeVisitorSupport; -import org.codehaus.groovy.ast.ConstructorNode; -import org.codehaus.groovy.ast.DynamicVariable; -import org.codehaus.groovy.ast.FieldNode; -import org.codehaus.groovy.ast.GenericsType; -import org.codehaus.groovy.ast.InnerClassNode; -import org.codehaus.groovy.ast.MethodNode; -import org.codehaus.groovy.ast.Parameter; -import org.codehaus.groovy.ast.PropertyNode; -import org.codehaus.groovy.ast.Variable; -import org.codehaus.groovy.ast.expr.ArgumentListExpression; -import org.codehaus.groovy.ast.expr.BinaryExpression; -import org.codehaus.groovy.ast.expr.ClassExpression; -import org.codehaus.groovy.ast.expr.ClosureExpression; -import org.codehaus.groovy.ast.expr.ConstantExpression; -import org.codehaus.groovy.ast.expr.ConstructorCallExpression; -import org.codehaus.groovy.ast.expr.Expression; -import org.codehaus.groovy.ast.expr.FieldExpression; -import org.codehaus.groovy.ast.expr.ListExpression; -import org.codehaus.groovy.ast.expr.MapEntryExpression; -import org.codehaus.groovy.ast.expr.MapExpression; -import org.codehaus.groovy.ast.expr.MethodCallExpression; -import org.codehaus.groovy.ast.expr.PropertyExpression; -import org.codehaus.groovy.ast.expr.TupleExpression; -import org.codehaus.groovy.ast.expr.VariableExpression; -import org.codehaus.groovy.ast.stmt.BlockStatement; -import org.codehaus.groovy.ast.stmt.EmptyStatement; -import org.codehaus.groovy.ast.stmt.ExpressionStatement; -import org.codehaus.groovy.ast.stmt.ReturnStatement; -import org.codehaus.groovy.ast.stmt.Statement; -import org.codehaus.groovy.ast.tools.GenericsUtils; -import org.codehaus.groovy.control.CompilationUnit; -import org.codehaus.groovy.control.CompilePhase; -import org.codehaus.groovy.control.CompilerConfiguration; -import org.codehaus.groovy.control.Phases; -import org.codehaus.groovy.control.SourceUnit; -import org.codehaus.groovy.control.messages.SyntaxErrorMessage; -import org.codehaus.groovy.runtime.DefaultGroovyMethods; -import org.codehaus.groovy.syntax.SyntaxException; -import org.codehaus.groovy.syntax.Types; -import org.codehaus.groovy.transform.ASTTransformation; -import org.codehaus.groovy.transform.GroovyASTTransformation; -import org.codehaus.groovy.transform.StaticTypesTransformation; -import org.codehaus.groovy.transform.sc.StaticCompileTransformation; - -import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.autoconfigure.AutoConfiguration; -import org.springframework.boot.autoconfigure.AutoConfigureAfter; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; -import org.springframework.boot.autoconfigure.AutoConfigureOrder; -import org.springframework.boot.autoconfigure.ImportAutoConfiguration; -import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; -import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression; -import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; -import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.ComponentScan; -import org.springframework.context.annotation.ComponentScans; -import org.springframework.context.annotation.Conditional; -import org.springframework.context.annotation.Configuration; -import org.springframework.context.annotation.Import; -import org.springframework.context.annotation.ImportResource; -import org.springframework.context.annotation.Lazy; -import org.springframework.context.annotation.Primary; -import org.springframework.context.annotation.PropertySource; -import org.springframework.context.annotation.PropertySources; -import org.springframework.context.annotation.Scope; - -import grails.compiler.beans.ConditionalOnGrailsEnv; -import grails.compiler.beans.GrailsBeans; +package org.grails.compiler.beans + +import java.beans.Introspector +import java.lang.reflect.Method +import java.lang.reflect.Modifier +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +import groovy.transform.CompilationUnitAware +import groovy.transform.CompileStatic +import groovy.transform.TypeChecked +import javax.lang.model.SourceVersion +import groovy.transform.TypeCheckingMode +import org.apache.groovy.util.BeanUtils +import org.codehaus.groovy.GroovyBugError +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.AnnotatedNode +import org.codehaus.groovy.ast.AnnotationNode +import org.codehaus.groovy.ast.AstToTextHelper +import org.codehaus.groovy.ast.ClassHelper +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.CodeVisitorSupport +import org.codehaus.groovy.ast.ConstructorNode +import org.codehaus.groovy.ast.DynamicVariable +import org.codehaus.groovy.ast.FieldNode +import org.codehaus.groovy.ast.GenericsType +import org.codehaus.groovy.ast.InnerClassNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.Parameter +import org.codehaus.groovy.ast.PropertyNode +import org.codehaus.groovy.ast.Variable +import org.codehaus.groovy.ast.expr.ArgumentListExpression +import org.codehaus.groovy.ast.expr.BinaryExpression +import org.codehaus.groovy.ast.expr.ClassExpression +import org.codehaus.groovy.ast.expr.ClosureExpression +import org.codehaus.groovy.ast.expr.ConstantExpression +import org.codehaus.groovy.ast.expr.ConstructorCallExpression +import org.codehaus.groovy.ast.expr.Expression +import org.codehaus.groovy.ast.expr.FieldExpression +import org.codehaus.groovy.ast.expr.ListExpression +import org.codehaus.groovy.ast.expr.MapEntryExpression +import org.codehaus.groovy.ast.expr.MapExpression +import org.codehaus.groovy.ast.expr.MethodCallExpression +import org.codehaus.groovy.ast.expr.PropertyExpression +import org.codehaus.groovy.ast.expr.TupleExpression +import org.codehaus.groovy.ast.expr.VariableExpression +import org.codehaus.groovy.ast.stmt.BlockStatement +import org.codehaus.groovy.ast.stmt.EmptyStatement +import org.codehaus.groovy.ast.stmt.ExpressionStatement +import org.codehaus.groovy.ast.stmt.ReturnStatement +import org.codehaus.groovy.ast.stmt.Statement +import org.codehaus.groovy.ast.tools.GenericsUtils +import org.codehaus.groovy.control.CompilationUnit +import org.codehaus.groovy.control.CompilePhase +import org.codehaus.groovy.control.CompilerConfiguration +import org.codehaus.groovy.control.Phases +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.control.messages.SyntaxErrorMessage +import org.codehaus.groovy.runtime.DefaultGroovyMethods +import org.codehaus.groovy.syntax.SyntaxException +import org.codehaus.groovy.syntax.Types +import org.codehaus.groovy.transform.ASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformation +import org.codehaus.groovy.transform.StaticTypesTransformation +import org.codehaus.groovy.transform.sc.StaticCompileTransformation +import org.springframework.beans.factory.annotation.Value +import org.springframework.boot.autoconfigure.AutoConfiguration +import org.springframework.boot.autoconfigure.AutoConfigureAfter +import org.springframework.boot.autoconfigure.AutoConfigureBefore +import org.springframework.boot.autoconfigure.AutoConfigureOrder +import org.springframework.boot.autoconfigure.ImportAutoConfiguration +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass +import org.springframework.boot.autoconfigure.condition.ConditionalOnExpression +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.ComponentScan +import org.springframework.context.annotation.ComponentScans +import org.springframework.context.annotation.Conditional +import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Import +import org.springframework.context.annotation.ImportResource +import org.springframework.context.annotation.Lazy +import org.springframework.context.annotation.Primary +import org.springframework.context.annotation.PropertySource +import org.springframework.context.annotation.PropertySources +import org.springframework.context.annotation.Scope + +import grails.compiler.beans.ConditionalOnGrailsEnv +import grails.compiler.beans.GrailsBeans /** * Rewrites the {@code beans} closure DSL on a {@link grails.compiler.beans.GrailsBeans}-annotated @@ -192,7 +178,8 @@ * that generate code after local transform discovery. */ @GroovyASTTransformation(phase = CompilePhase.CANONICALIZATION) -public class GrailsBeansASTTransformation implements ASTTransformation, CompilationUnitAware { +@CompileStatic +class GrailsBeansASTTransformation implements ASTTransformation, CompilationUnitAware { /** * Class-node metadata carrying the compilation's output directory, seeded by the global Grails @@ -201,90 +188,88 @@ public class GrailsBeansASTTransformation implements ASTTransformation, Compilat * grails-core - which this module cannot depend on. */ public static final String RESOLVED_TARGET_DIRECTORY_METADATA = - GrailsBeansASTTransformation.class.getName() + ".resolvedTargetDirectory"; + GrailsBeansASTTransformation.getName() + '.resolvedTargetDirectory' - private static final String BEANS_PROPERTY = "beans"; - private static final String BEAN_CALL = "bean"; - private static final String FIELD_CALL = "field"; - private static final String METHOD_CALL = "method"; - private static final String GROUP_CALL = "group"; + private static final String BEANS_PROPERTY = 'beans' + private static final String BEAN_CALL = 'bean' + private static final String FIELD_CALL = 'field' + private static final String METHOD_CALL = 'method' + private static final String GROUP_CALL = 'group' private static final Set ROOT_STATEMENT_CALL_NAMES = - Set.of(BEAN_CALL, FIELD_CALL, METHOD_CALL, GROUP_CALL); - private static final String CONDITIONAL_ON_BEAN_CALL = "conditionalOnBean"; - private static final String CONDITIONAL_ON_MISSING_BEAN_CALL = "conditionalOnMissingBean"; - private static final String CONDITIONAL_ON_MISSING_BEAN_NAME_CALL = "conditionalOnMissingBeanName"; - private static final String CONDITIONAL_ON_PROPERTY_CALL = "conditionalOnProperty"; - private static final String CONDITIONAL_ON_EXPRESSION_CALL = "conditionalOnExpression"; - private static final String CONDITIONAL_ON_CLASS_CALL = "conditionalOnClass"; - private static final String PRIMARY_CALL = "primary"; - private static final String LAZY_CALL = "lazy"; - private static final String SCOPE_CALL = "scope"; - private static final String STATIC_METHOD_CALL = "staticMethod"; - private static final String ANNOTATE_CALL = "annotate"; - private static final String VALUE_CALL = "value"; - private static final String TYPE_ARGUMENTS_CALL = "typeArguments"; - private static final String ALIASES_CALL = "aliases"; - private static final String CONDITIONAL_ON_GRAILS_ENV_CALL = "conditionalOnGrailsEnv"; + Set.of(BEAN_CALL, FIELD_CALL, METHOD_CALL, GROUP_CALL) + private static final String CONDITIONAL_ON_BEAN_CALL = 'conditionalOnBean' + private static final String CONDITIONAL_ON_MISSING_BEAN_CALL = 'conditionalOnMissingBean' + private static final String CONDITIONAL_ON_MISSING_BEAN_NAME_CALL = 'conditionalOnMissingBeanName' + private static final String CONDITIONAL_ON_PROPERTY_CALL = 'conditionalOnProperty' + private static final String CONDITIONAL_ON_EXPRESSION_CALL = 'conditionalOnExpression' + private static final String CONDITIONAL_ON_CLASS_CALL = 'conditionalOnClass' + private static final String PRIMARY_CALL = 'primary' + private static final String LAZY_CALL = 'lazy' + private static final String SCOPE_CALL = 'scope' + private static final String STATIC_METHOD_CALL = 'staticMethod' + private static final String ANNOTATE_CALL = 'annotate' + private static final String VALUE_CALL = 'value' + private static final String TYPE_ARGUMENTS_CALL = 'typeArguments' + private static final String ALIASES_CALL = 'aliases' + private static final String CONDITIONAL_ON_GRAILS_ENV_CALL = 'conditionalOnGrailsEnv' private static final Set BEAN_QUALIFIER_CALL_NAMES = Set.of( CONDITIONAL_ON_BEAN_CALL, CONDITIONAL_ON_MISSING_BEAN_CALL, CONDITIONAL_ON_MISSING_BEAN_NAME_CALL, CONDITIONAL_ON_PROPERTY_CALL, CONDITIONAL_ON_EXPRESSION_CALL, CONDITIONAL_ON_CLASS_CALL, PRIMARY_CALL, LAZY_CALL, SCOPE_CALL, STATIC_METHOD_CALL, - ANNOTATE_CALL, TYPE_ARGUMENTS_CALL, CONDITIONAL_ON_GRAILS_ENV_CALL, ALIASES_CALL); + ANNOTATE_CALL, TYPE_ARGUMENTS_CALL, CONDITIONAL_ON_GRAILS_ENV_CALL, ALIASES_CALL) // field(...) and method(...) declare plain class members, not beans - bean-specific // qualifiers don't apply; .value(...) (@Value config injection) is field-only. - private static final Set FIELD_QUALIFIER_CALL_NAMES = Set.of(ANNOTATE_CALL, VALUE_CALL, TYPE_ARGUMENTS_CALL); - private static final Set METHOD_QUALIFIER_CALL_NAMES = Set.of(ANNOTATE_CALL, TYPE_ARGUMENTS_CALL); + private static final Set FIELD_QUALIFIER_CALL_NAMES = Set.of(ANNOTATE_CALL, VALUE_CALL, TYPE_ARGUMENTS_CALL) + private static final Set METHOD_QUALIFIER_CALL_NAMES = Set.of(ANNOTATE_CALL, TYPE_ARGUMENTS_CALL) // A group declares a nested configuration class, so it takes what applies to a class: the // conditions, and the escape hatch. Bean-shaped qualifiers (primary, scope, aliases, the // type arguments of a declared type) have nothing to attach to here. private static final Set GROUP_QUALIFIER_CALL_NAMES = Set.of( CONDITIONAL_ON_BEAN_CALL, CONDITIONAL_ON_MISSING_BEAN_CALL, CONDITIONAL_ON_PROPERTY_CALL, CONDITIONAL_ON_EXPRESSION_CALL, CONDITIONAL_ON_CLASS_CALL, CONDITIONAL_ON_GRAILS_ENV_CALL, - ANNOTATE_CALL); + ANNOTATE_CALL) // Every qualifier any declaration accepts. Derived, not restated: this set decides whether a // chained call is a qualifier at all, so a name present in one of the three sets above but // missing here would be rejected by the chain walk as if the whole statement were malformed - // "Expected bean([\"name\", ] Type)..." pointing at a qualifier that is in fact supported. private static final Set ALL_QUALIFIER_CALL_NAMES = - Stream.of(BEAN_QUALIFIER_CALL_NAMES, FIELD_QUALIFIER_CALL_NAMES, METHOD_QUALIFIER_CALL_NAMES) - .flatMap(Set::stream) - .collect(Collectors.toUnmodifiableSet()); - private static final String PLUGIN_SUPERCLASS_NAME = "grails.plugins.Plugin"; - private static final String GRAILS_PLUGIN_SUFFIX = "GrailsPlugin"; - private static final String AUTO_CONFIGURATION_SUFFIX = "AutoConfiguration"; - private static final String AUTO_CONFIGURATION_NAME_MEMBER = "autoConfigurationName"; - private static final String MOVE_ANNOTATIONS_MEMBER = "moveAnnotations"; - private static final String PROXY_BEAN_METHODS_MEMBER = "proxyBeanMethods"; + Set.copyOf(BEAN_QUALIFIER_CALL_NAMES + FIELD_QUALIFIER_CALL_NAMES + METHOD_QUALIFIER_CALL_NAMES) + private static final String PLUGIN_SUPERCLASS_NAME = 'grails.plugins.Plugin' + private static final String GRAILS_PLUGIN_SUFFIX = 'GrailsPlugin' + private static final String AUTO_CONFIGURATION_SUFFIX = 'AutoConfiguration' + private static final String AUTO_CONFIGURATION_NAME_MEMBER = 'autoConfigurationName' + private static final String MOVE_ANNOTATIONS_MEMBER = 'moveAnnotations' + private static final String PROXY_BEAN_METHODS_MEMBER = 'proxyBeanMethods' /** Named, not referenced: this module does not depend on the testing support. */ - static final String UNIT_TEST_TRAIT_NAME = "org.grails.testing.GrailsUnitTest"; + static final String UNIT_TEST_TRAIT_NAME = 'org.grails.testing.GrailsUnitTest' /** What Spock renames a {@code @Shared beans} field to (its InternalIdentifiers.getSharedFieldName). */ - static final String SPOCK_SHARED_BEANS_FIELD = "$spock_sharedField_" + BEANS_PROPERTY; - private static final String DUMP_DIR_PROPERTY = "grails.beans.dsl.dumpdir"; + static final String SPOCK_SHARED_BEANS_FIELD = '\$spock_sharedField_' + BEANS_PROPERTY + private static final String DUMP_DIR_PROPERTY = 'grails.beans.dsl.dumpdir' - private CompilationUnit compilationUnit; + private CompilationUnit compilationUnit @Override - public void setCompilationUnit(CompilationUnit compilationUnit) { - this.compilationUnit = compilationUnit; + void setCompilationUnit(CompilationUnit compilationUnit) { + this.compilationUnit = compilationUnit } @Override - public void visit(ASTNode[] nodes, SourceUnit source) { - AnnotationNode grailsBeansAnnotation = (AnnotationNode) nodes[0]; - ClassNode classNode = (ClassNode) nodes[1]; - PropertyNode beansProperty = classNode.getProperty(BEANS_PROPERTY); + void visit(ASTNode[] nodes, SourceUnit source) { + AnnotationNode grailsBeansAnnotation = (AnnotationNode) nodes[0] + ClassNode classNode = (ClassNode) nodes[1] + PropertyNode beansProperty = classNode.getProperty(BEANS_PROPERTY) if (beansProperty == null) { if (!reportSharedBeans(classNode, source)) { - addError(classNode, source, "@GrailsBeans requires a 'beans' property initialised to a closure"); + addError(classNode, source, "@GrailsBeans requires a 'beans' property initialised to a closure") } - return; + return } - reclaimMovedInitializer(classNode, beansProperty); + reclaimMovedInitializer(classNode, beansProperty) - Expression initialExpression = beansProperty.getInitialExpression(); + Expression initialExpression = beansProperty.getInitialExpression() if (!(initialExpression instanceof ClosureExpression)) { - addError(beansProperty, source, "'beans' must be initialised to a closure, e.g. beans = { ... }"); - return; + addError(beansProperty, source, "'beans' must be initialised to a closure, e.g. beans = { ... }") + return } // An empty block is a no-op, not an error - an empty @Configuration class is legal in Spring @@ -292,85 +277,85 @@ public void visit(ASTNode[] nodes, SourceUnit source) { // fail the build. Returning before the sibling is created matters: generating it would leave // a bean-less class holding the @AutoConfiguration and @Conditional* annotations moved off // the plugin, which is worse than doing nothing. Only the DSL scaffolding is stripped. - List statements = beanStatements((ClosureExpression) initialExpression); + List statements = beanStatements((ClosureExpression) initialExpression) if (statements.isEmpty()) { - removeBeansProperty(classNode, beansProperty); - return; + removeBeansProperty(classNode, beansProperty) + return } - boolean isPlugin = extendsGrailsPlugin(classNode); - boolean isUnitTest = !isPlugin && isUnitTest(classNode); + boolean isPlugin = extendsGrailsPlugin(classNode) + boolean isUnitTest = !isPlugin && isUnitTest(classNode) if (!isPlugin) { - for (String pluginOnlyMember : new String[] { AUTO_CONFIGURATION_NAME_MEMBER, MOVE_ANNOTATIONS_MEMBER }) { + for (String pluginOnlyMember in [AUTO_CONFIGURATION_NAME_MEMBER, MOVE_ANNOTATIONS_MEMBER] as String[]) { if (grailsBeansAnnotation.getMember(pluginOnlyMember) != null) { - addError(grailsBeansAnnotation, source, pluginOnlyMember + " has no effect here: it only applies " + - "when @GrailsBeans is applied to a grails.plugins.Plugin subclass, where the compiled beans " + - "land on a generated sibling class rather than on " + classNode.getNameWithoutPackage() + " itself"); + addError(grailsBeansAnnotation, source, pluginOnlyMember + ' has no effect here: it only applies ' + + 'when @GrailsBeans is applied to a grails.plugins.Plugin subclass, where the compiled beans ' + + 'land on a generated sibling class rather than on ' + classNode.getNameWithoutPackage() + ' itself') } } } - ClassNode beanMethodHost; + ClassNode beanMethodHost if (isPlugin) { - beanMethodHost = createAutoConfigurationSibling(classNode, grailsBeansAnnotation, source); + beanMethodHost = createAutoConfigurationSibling(classNode, grailsBeansAnnotation, source) } else if (isUnitTest) { - beanMethodHost = createUnitTestConfiguration(classNode, beansProperty, source); + beanMethodHost = createUnitTestConfiguration(classNode, beansProperty, source) if (beanMethodHost == null) { - return; + return } } else { - beanMethodHost = classNode; + beanMethodHost = classNode } - Set usedNames = existingMemberNames(beanMethodHost); - validateSharedBeanNames(statements, classNode, source); + Set usedNames = existingMemberNames(beanMethodHost) + validateSharedBeanNames(statements, classNode, source) // Two passes: field(...)/method(...) declare explicit member names, so they are processed // first (along with anything malformed, so every statement is still processed exactly // once) and bean(...) statements second. A bean's derived method name then adapts to every // explicitly-named member wherever it appears in the block - reordering equivalent DSL // statements must never change validity. - List preExistingMethods = new ArrayList<>(beanMethodHost.getMethods()); - List preExistingFields = new ArrayList<>(beanMethodHost.getFields()); - for (Statement statement : statements) { + List preExistingMethods = new ArrayList<>(beanMethodHost.getMethods()) + List preExistingFields = new ArrayList<>(beanMethodHost.getFields()) + for (Statement statement in statements) { if (!isBeanRootedStatement(statement)) { - processStatement(beanMethodHost, classNode, statement, source, usedNames); + processStatement(beanMethodHost, classNode, statement, source, usedNames) } } - for (Statement statement : statements) { + for (Statement statement in statements) { if (isBeanRootedStatement(statement)) { - processStatement(beanMethodHost, classNode, statement, source, usedNames); + processStatement(beanMethodHost, classNode, statement, source, usedNames) } } - List generatedMethods = generatedMembers(beanMethodHost, preExistingMethods); - List generatedFields = new ArrayList<>(beanMethodHost.getFields()); - generatedFields.removeAll(preExistingFields); - rejectUnproxiedSiblingBeanCalls(beanMethodHost, generatedMethods, source); + List generatedMethods = generatedMembers(beanMethodHost, preExistingMethods) + List generatedFields = new ArrayList<>(beanMethodHost.getFields()) + generatedFields.removeAll(preExistingFields) + rejectUnproxiedSiblingBeanCalls(beanMethodHost, generatedMethods, source) if (beanMethodHost != classNode) { // Only on a plugin descriptor or a unit test. On a plain host the beans and the anonymous // class share a home, so this$0 is never retyped and an unqualified reference resolves as // it reads. - rejectAnonymousClassReachingOutward(beanMethodHost, isUnitTest, generatedMethods, source); + rejectAnonymousClassReachingOutward(beanMethodHost, isUnitTest, generatedMethods, source) } - dumpGeneratedMembers(beanMethodHost, generatedMethods, generatedFields, source); + dumpGeneratedMembers(beanMethodHost, generatedMethods, generatedFields, source) if (beanMethodHost != classNode) { // Deferred for the same reason a group's nested class is: this pass reaches any nested // class the sibling now holds, and marking one before Groovy has added its inner-class // MOP methods fails class generation. - deferStaticCompilation(classNode, beanMethodHost, source); + deferStaticCompilation(classNode, beanMethodHost, source) } - removeBeansProperty(classNode, beansProperty); + removeBeansProperty(classNode, beansProperty) } private void removeBeansProperty(ClassNode classNode, PropertyNode beansProperty) { - classNode.getProperties().remove(beansProperty); + classNode.getProperties().remove(beansProperty) // removeField, not getFields().remove: the latter leaves ClassNode's own fieldIndex entry // behind, so another member still referring to 'beans' type-checks and compiles to a // getfield against a field that is never emitted, failing with NoSuchFieldError at runtime. - classNode.removeField(BEANS_PROPERTY); + classNode.removeField(BEANS_PROPERTY) } /** @@ -380,15 +365,15 @@ private void removeBeansProperty(ClassNode classNode, PropertyNode beansProperty * and the statement would outlive the field this removes. The statement is found by the field * it assigns, not by where Spock put it. */ - public static void reclaimMovedInitializer(ClassNode classNode, PropertyNode beansProperty) { - FieldNode field = beansProperty.getField(); + static void reclaimMovedInitializer(ClassNode classNode, PropertyNode beansProperty) { + FieldNode field = beansProperty.getField() if (field == null || field.getInitialExpression() != null) { - return; + return } - MovedInitializer moved = findMovedInitializer(classNode, field); + MovedInitializer moved = findMovedInitializer(classNode, field) if (moved != null) { - moved.block.getStatements().remove(moved.statement); - field.setInitialValueExpression(moved.closure); + moved.block.getStatements().remove(moved.statement) + field.setInitialValueExpression(moved.closure) } } @@ -396,55 +381,55 @@ public static void reclaimMovedInitializer(ClassNode classNode, PropertyNode bea * The closure Spock moved off {@code field} into one of its generated methods, left where it is; * {@code null} when there is none. For deciding whether a block is the DSL before claiming it. */ - public static ClosureExpression movedInitializer(ClassNode classNode, FieldNode field) { - MovedInitializer moved = field == null ? null : findMovedInitializer(classNode, field); - return moved == null ? null : moved.closure; + static ClosureExpression movedInitializer(ClassNode classNode, FieldNode field) { + MovedInitializer moved = field == null ? null : findMovedInitializer(classNode, field) + return moved == null ? null : moved.closure } private static MovedInitializer findMovedInitializer(ClassNode classNode, FieldNode field) { - for (MethodNode method : classNode.getMethods()) { + for (MethodNode method in classNode.getMethods()) { if (method.getCode() instanceof BlockStatement) { - MovedInitializer moved = findMovedInitializer((BlockStatement) method.getCode(), field); + MovedInitializer moved = findMovedInitializer((BlockStatement) method.getCode(), field) if (moved != null) { - return moved; + return moved } } } - return null; + return null } private static MovedInitializer findMovedInitializer(BlockStatement block, FieldNode field) { - for (Statement statement : block.getStatements()) { + for (Statement statement in block.getStatements()) { if (statement instanceof BlockStatement) { - MovedInitializer nested = findMovedInitializer((BlockStatement) statement, field); + MovedInitializer nested = findMovedInitializer((BlockStatement) statement, field) if (nested != null) { - return nested; + return nested } } if (!(statement instanceof ExpressionStatement) || !(((ExpressionStatement) statement).getExpression() instanceof BinaryExpression)) { - continue; + continue } - BinaryExpression assignment = (BinaryExpression) ((ExpressionStatement) statement).getExpression(); + BinaryExpression assignment = (BinaryExpression) ((ExpressionStatement) statement).getExpression() if (assignment.getOperation().getType() == Types.ASSIGN && assignment.getLeftExpression() instanceof FieldExpression && ((FieldExpression) assignment.getLeftExpression()).getField() == field && assignment.getRightExpression() instanceof ClosureExpression) { - return new MovedInitializer(block, statement, (ClosureExpression) assignment.getRightExpression()); + return new MovedInitializer(block, statement, (ClosureExpression) assignment.getRightExpression()) } } - return null; + return null } private static final class MovedInitializer { - private final BlockStatement block; - private final Statement statement; - private final ClosureExpression closure; + private final BlockStatement block + private final Statement statement + private final ClosureExpression closure private MovedInitializer(BlockStatement block, Statement statement, ClosureExpression closure) { - this.block = block; - this.statement = statement; - this.closure = closure; + this.block = block + this.statement = statement + this.closure = closure } } @@ -453,23 +438,23 @@ private MovedInitializer(BlockStatement block, Statement statement, ClosureExpre * {@code method} or {@code group} call, looking through chained qualifiers to the root call. */ private static boolean declaresBeans(ClosureExpression closure) { - Statement code = closure.getCode(); + Statement code = closure.getCode() List statements = code instanceof BlockStatement ? - ((BlockStatement) code).getStatements() : Collections.singletonList(code); - for (Statement statement : statements) { + ((BlockStatement) code).getStatements() : Collections.singletonList(code) + for (Statement statement in statements) { if (!(statement instanceof ExpressionStatement)) { - continue; + continue } - Expression expression = ((ExpressionStatement) statement).getExpression(); + Expression expression = ((ExpressionStatement) statement).getExpression() while (expression instanceof MethodCallExpression) { - MethodCallExpression call = (MethodCallExpression) expression; + MethodCallExpression call = (MethodCallExpression) expression if (ROOT_STATEMENT_CALL_NAMES.contains(call.getMethodAsString())) { - return true; + return true } - expression = call.getObjectExpression(); + expression = call.getObjectExpression() } } - return false; + return false } /** @@ -480,20 +465,20 @@ private static boolean declaresBeans(ClosureExpression closure) { * * @return whether there was one to report */ - public static boolean reportSharedBeans(ClassNode classNode, SourceUnit source) { - FieldNode shared = classNode.getDeclaredField(SPOCK_SHARED_BEANS_FIELD); - ClosureExpression closure = movedInitializer(classNode, shared); + static boolean reportSharedBeans(ClassNode classNode, SourceUnit source) { + FieldNode shared = classNode.getDeclaredField(SPOCK_SHARED_BEANS_FIELD) + ClosureExpression closure = movedInitializer(classNode, shared) // A @Shared beans field holding something other than the DSL is left alone, as an // instance one is if (closure == null || !declaresBeans(closure)) { - return false; + return false } source.getErrorCollector().addErrorAndContinue(new SyntaxErrorMessage(new SyntaxException( "A unit test's 'beans' block cannot be @Shared - Spock moves a shared field where the beans " + - "DSL cannot follow it. Remove @Shared: the beans are created once for the test class " + - "whether or not it is there.", - shared.getLineNumber(), shared.getColumnNumber()), source)); - return true; + 'DSL cannot follow it. Remove @Shared: the beans are created once for the test class ' + + 'whether or not it is there.', + shared.getLineNumber(), shared.getColumnNumber()), source)) + return true } /** @@ -501,8 +486,8 @@ public static boolean reportSharedBeans(ClassNode classNode, SourceUnit source) * testing trait does. Checked with {@link ClassNode#implementsInterface}, which follows every * super-interface and superclass, so a spec inheriting the trait from a base spec counts. */ - public static boolean isUnitTest(ClassNode classNode) { - return classNode.implementsInterface(ClassHelper.make(UNIT_TEST_TRAIT_NAME)); + static boolean isUnitTest(ClassNode classNode) { + return classNode.implementsInterface(ClassHelper.make(UNIT_TEST_TRAIT_NAME)) } /** @@ -515,35 +500,35 @@ public static boolean isUnitTest(ClassNode classNode) { * @return the class, or {@code null} after reporting that the test already declares one of that name */ private ClassNode createUnitTestConfiguration(ClassNode testClass, PropertyNode beansProperty, SourceUnit source) { - String name = testClass.getName() + "$" + GrailsBeans.UNIT_TEST_CONFIGURATION_NAME; - for (ClassNode existing : source.getAST().getClasses()) { + String name = testClass.getName() + '\$' + GrailsBeans.UNIT_TEST_CONFIGURATION_NAME + for (ClassNode existing in source.getAST().getClasses()) { if (existing.getName().equals(name)) { - addError(beansProperty, source, testClass.getNameWithoutPackage() + " already declares a nested " + - "class named " + GrailsBeans.UNIT_TEST_CONFIGURATION_NAME + " - a unit test's beans compile onto a " + - "nested class of that name, so rename the existing one"); - return null; + addError(beansProperty, source, testClass.getNameWithoutPackage() + ' already declares a nested ' + + 'class named ' + GrailsBeans.UNIT_TEST_CONFIGURATION_NAME + " - a unit test's beans compile onto a " + + 'nested class of that name, so rename the existing one') + return null } } InnerClassNode configuration = new InnerClassNode(testClass, name, - Modifier.PUBLIC | Modifier.STATIC, ClassHelper.OBJECT_TYPE); - configuration.setSourcePosition(beansProperty); - source.getAST().addClass(configuration); + Modifier.PUBLIC | Modifier.STATIC, ClassHelper.OBJECT_TYPE) + configuration.setSourcePosition(beansProperty) + source.getAST().addClass(configuration) // proxyBeanMethods = false, as a group's nested class has: nothing here needs CGLIB, and it // keeps the sibling-call check meaningful. - AnnotationNode annotation = new AnnotationNode(ClassHelper.make(Configuration.class)); - annotation.setMember(PROXY_BEAN_METHODS_MEMBER, new ConstantExpression(Boolean.FALSE)); - configuration.addAnnotation(withPosition(annotation, beansProperty)); - return configuration; + AnnotationNode annotation = new AnnotationNode(ClassHelper.make(Configuration)) + annotation.setMember(PROXY_BEAN_METHODS_MEMBER, new ConstantExpression(Boolean.FALSE)) + configuration.addAnnotation(withPosition(annotation, beansProperty)) + return configuration } private boolean extendsGrailsPlugin(ClassNode classNode) { for (ClassNode current = classNode.getSuperClass(); current != null; current = current.getSuperClass()) { if (PLUGIN_SUPERCLASS_NAME.equals(current.getName())) { - return true; + return true } } - return false; + return false } // Annotations that only make sense on whatever class Spring Boot actually evaluates as an @@ -554,47 +539,47 @@ private boolean extendsGrailsPlugin(ClassNode classNode) { // @ConditionalOnFeature meta-annotated with Spring Boot's own @ConditionalOnProperty, or a // custom @EnableSomething meta-annotated with @Import) is found automatically. private static final Set SIBLING_ONLY_ANNOTATION_NAMES = Set.of( - AutoConfiguration.class.getName(), AutoConfigureOrder.class.getName(), - AutoConfigureBefore.class.getName(), AutoConfigureAfter.class.getName(), - Import.class.getName(), ImportAutoConfiguration.class.getName(), ImportResource.class.getName(), - ComponentScan.class.getName(), ComponentScans.class.getName(), - EnableConfigurationProperties.class.getName(), - PropertySource.class.getName(), PropertySources.class.getName(), - Conditional.class.getName()); + AutoConfiguration.getName(), AutoConfigureOrder.getName(), + AutoConfigureBefore.getName(), AutoConfigureAfter.getName(), + Import.getName(), ImportAutoConfiguration.getName(), ImportResource.getName(), + ComponentScan.getName(), ComponentScans.getName(), + EnableConfigurationProperties.getName(), + PropertySource.getName(), PropertySources.getName(), + Conditional.getName()) private ClassNode createAutoConfigurationSibling(ClassNode pluginClass, AnnotationNode grailsBeansAnnotation, SourceUnit source) { - List autoConfigurationAnnotations = pluginClass.getAnnotations(ClassHelper.make(AutoConfiguration.class)); + List autoConfigurationAnnotations = pluginClass.getAnnotations(ClassHelper.make(AutoConfiguration)) if (autoConfigurationAnnotations.isEmpty()) { - addError(pluginClass, source, "A Plugin class using @GrailsBeans must also be annotated " + - "@AutoConfiguration (even with no before=/after=) - otherwise the generated " + + addError(pluginClass, source, 'A Plugin class using @GrailsBeans must also be annotated ' + + '@AutoConfiguration (even with no before=/after=) - otherwise the generated ' + defaultSiblingSimpleName(pluginClass) + - " class would never be processed by Spring Boot"); + ' class would never be processed by Spring Boot') } - String siblingName = siblingName(pluginClass, grailsBeansAnnotation, source); - ClassNode sibling = new ClassNode(siblingName, Modifier.PUBLIC, ClassHelper.OBJECT_TYPE); + String siblingName = siblingName(pluginClass, grailsBeansAnnotation, source) + ClassNode sibling = new ClassNode(siblingName, Modifier.PUBLIC, ClassHelper.OBJECT_TYPE) // Without a position, anything Groovy later reports against a generated node - a sibling // name clash, a typo in .annotate(...) - is reported at line -1, column -1. - sibling.setSourcePosition(pluginClass); - source.getAST().addClass(sibling); + sibling.setSourcePosition(pluginClass) + source.getAST().addClass(sibling) // Matching annotations move entirely rather than being merely copied - they have no effect // where the author wrote them (see SIBLING_ONLY_ANNOTATION_NAMES). - Set moveAnnotationNames = parseMoveAnnotations(grailsBeansAnnotation, source); - List siblingAnnotations = new ArrayList<>(); - for (AnnotationNode annotation : pluginClass.getAnnotations()) { + Set moveAnnotationNames = parseMoveAnnotations(grailsBeansAnnotation, source) + List siblingAnnotations = new ArrayList<>() + for (AnnotationNode annotation in pluginClass.getAnnotations()) { if (belongsOnSibling(annotation.getClassNode(), moveAnnotationNames)) { - siblingAnnotations.add(annotation); + siblingAnnotations.add(annotation) } } - sibling.addAnnotations(siblingAnnotations); - pluginClass.getAnnotations().removeAll(siblingAnnotations); + sibling.addAnnotations(siblingAnnotations) + pluginClass.getAnnotations().removeAll(siblingAnnotations) // The name is settled here and nowhere else, so this is where it is registered. AutoConfigurationImportsWriter.register( - siblingName, targetDirectory(pluginClass, source), source, compilationUnit); + siblingName, targetDirectory(pluginClass, source), source, compilationUnit) - return sibling; + return sibling } /** @@ -602,47 +587,47 @@ private ClassNode createAutoConfigurationSibling(ClassNode pluginClass, Annotati * the only one that is right under Groovy-Eclipse, and the compiler's own otherwise. */ private static File targetDirectory(ClassNode classNode, SourceUnit source) { - Object resolved = classNode == null ? null : classNode.getNodeMetaData(RESOLVED_TARGET_DIRECTORY_METADATA); + Object resolved = classNode == null ? null : classNode.getNodeMetaData(RESOLVED_TARGET_DIRECTORY_METADATA) if (resolved instanceof File) { - return (File) resolved; + return (File) resolved } - CompilerConfiguration configuration = source == null ? null : source.getConfiguration(); - return configuration == null ? null : configuration.getTargetDirectory(); + CompilerConfiguration configuration = source == null ? null : source.getConfiguration() + return configuration == null ? null : configuration.getTargetDirectory() } private Set parseMoveAnnotations(AnnotationNode grailsBeansAnnotation, SourceUnit source) { - Expression member = grailsBeansAnnotation.getMember(MOVE_ANNOTATIONS_MEMBER); + Expression member = grailsBeansAnnotation.getMember(MOVE_ANNOTATIONS_MEMBER) if (member == null) { - return Set.of(); + return Set.of() } List entries = member instanceof ListExpression ? - ((ListExpression) member).getExpressions() : List.of(member); - Set names = new HashSet<>(); - for (Expression entry : entries) { + ((ListExpression) member).getExpressions() : List.of(member) + Set names = new HashSet<>() + for (Expression entry in entries) { if (!(entry instanceof ClassExpression)) { - addError(entry, source, "moveAnnotations entries must be annotation class literals, " + - "e.g. @GrailsBeans(moveAnnotations = [ComponentScan])"); - continue; + addError(entry, source, 'moveAnnotations entries must be annotation class literals, ' + + 'e.g. @GrailsBeans(moveAnnotations = [ComponentScan])') + continue } - ClassNode annotationType = ((ClassExpression) entry).getType(); + ClassNode annotationType = ((ClassExpression) entry).getType() if (!annotationType.isAnnotationDefinition()) { - addError(entry, source, "\"" + annotationType.getName() + "\" is not an annotation type"); - continue; + addError(entry, source, '"' + annotationType.getName() + '" is not an annotation type') + continue } - names.add(annotationType.getName()); + names.add(annotationType.getName()) } - return names; + return names } // A *GrailsPlugin name swaps that suffix for AutoConfiguration (I18nGrailsPlugin -> // I18nAutoConfiguration - the name the hand-written class it replaces would have had); // anything else appends AutoConfiguration. private String defaultSiblingSimpleName(ClassNode pluginClass) { - String simpleName = pluginClass.getNameWithoutPackage(); + String simpleName = pluginClass.getNameWithoutPackage() if (simpleName.endsWith(GRAILS_PLUGIN_SUFFIX) && simpleName.length() > GRAILS_PLUGIN_SUFFIX.length()) { - return simpleName.substring(0, simpleName.length() - GRAILS_PLUGIN_SUFFIX.length()) + AUTO_CONFIGURATION_SUFFIX; + return simpleName.substring(0, simpleName.length() - GRAILS_PLUGIN_SUFFIX.length()) + AUTO_CONFIGURATION_SUFFIX } - return simpleName + AUTO_CONFIGURATION_SUFFIX; + return simpleName + AUTO_CONFIGURATION_SUFFIX } // The name of the generated sibling, qualified. A bare identifier names it in the plugin's own @@ -650,38 +635,38 @@ private String defaultSiblingSimpleName(ClassNode pluginClass) { // written, so a conversion can keep the package of the class it replaces as well as its simple // name - the two together being what an exclude= or a before=/after= from another module names. private String siblingName(ClassNode pluginClass, AnnotationNode grailsBeansAnnotation, SourceUnit source) { - String packageName = pluginClass.getPackageName(); - String defaultName = qualify(packageName, defaultSiblingSimpleName(pluginClass)); - Expression nameArg = grailsBeansAnnotation.getMember(AUTO_CONFIGURATION_NAME_MEMBER); + String packageName = pluginClass.getPackageName() + String defaultName = qualify(packageName, defaultSiblingSimpleName(pluginClass)) + Expression nameArg = grailsBeansAnnotation.getMember(AUTO_CONFIGURATION_NAME_MEMBER) if (nameArg == null) { - return defaultName; + return defaultName } - Object nameValue = nameArg instanceof ConstantExpression ? ((ConstantExpression) nameArg).getValue() : null; + Object nameValue = nameArg instanceof ConstantExpression ? ((ConstantExpression) nameArg).getValue() : null if (!(nameValue instanceof String)) { - addError(nameArg, source, "@GrailsBeans(autoConfigurationName = ...) requires a String literal"); - return defaultName; + addError(nameArg, source, '@GrailsBeans(autoConfigurationName = ...) requires a String literal') + return defaultName } - String name = (String) nameValue; + String name = (String) nameValue if (name.isBlank()) { - addError(nameArg, source, "@GrailsBeans(autoConfigurationName = \"" + name + "\") must not be " + - "blank - omit the attribute entirely to use the default " + defaultName + " instead"); - return defaultName; + addError(nameArg, source, '@GrailsBeans(autoConfigurationName = "' + name + '") must not be ' + + 'blank - omit the attribute entirely to use the default ' + defaultName + ' instead') + return defaultName } if (!isValidQualifiedName(name)) { - addError(nameArg, source, "@GrailsBeans(autoConfigurationName = \"" + name + "\") is not a valid " + + addError(nameArg, source, '@GrailsBeans(autoConfigurationName = "' + name + '") is not a valid ' + "name: it becomes the generated sibling's class name, so it must be a valid Java " + - "identifier, or a qualified name whose every part is one"); - return defaultName; + 'identifier, or a qualified name whose every part is one') + return defaultName } - return name.indexOf('.') < 0 ? qualify(packageName, name) : name; + return name.indexOf('.') < 0 ? qualify(packageName, name) : name } private static String qualify(String packageName, String simpleName) { - return (packageName == null || packageName.isEmpty()) ? simpleName : packageName + "." + simpleName; + return (packageName == null || packageName.isEmpty()) ? simpleName : packageName + '.' + simpleName } private boolean belongsOnSibling(ClassNode annotationType, Set moveAnnotationNames) { - return belongsOnSibling(annotationType, moveAnnotationNames, new HashSet<>()); + return belongsOnSibling(annotationType, moveAnnotationNames, new HashSet<>()) } // Recurses through meta-annotations rather than checking only one level, since Spring's own @@ -692,18 +677,18 @@ private boolean belongsOnSibling(ClassNode annotationType, Set moveAnnot // (@Retention, @Target, @Documented) from multiple paths, avoids redundant re-exploration. private boolean belongsOnSibling(ClassNode annotationType, Set moveAnnotationNames, Set visited) { if (!visited.add(annotationType.getName())) { - return false; + return false } if (SIBLING_ONLY_ANNOTATION_NAMES.contains(annotationType.getName()) || moveAnnotationNames.contains(annotationType.getName())) { - return true; + return true } - for (AnnotationNode metaAnnotation : annotationType.getAnnotations()) { + for (AnnotationNode metaAnnotation in annotationType.getAnnotations()) { if (belongsOnSibling(metaAnnotation.getClassNode(), moveAnnotationNames, visited)) { - return true; + return true } } - return false; + return false } // The bean bodies are lifted onto a class the normal transform pipeline no longer visits, so @@ -712,14 +697,14 @@ private boolean belongsOnSibling(ClassNode annotationType, Set moveAnnot // own: @AnnotationCollector has already expanded them by canonicalization. private void applyStaticCompilation(ClassNode pluginClass, ClassNode sibling, SourceUnit source) { if (compilationUnit == null) { - return; + return } if (applyStaticTypesTransformation(pluginClass, sibling, source, - CompileStatic.class, new StaticCompileTransformation())) { - return; + CompileStatic, new StaticCompileTransformation())) { + return } applyStaticTypesTransformation(pluginClass, sibling, source, - TypeChecked.class, new StaticTypesTransformation()); + TypeChecked, new StaticTypesTransformation()) } /** @@ -732,42 +717,42 @@ CompileStatic.class, new StaticCompileTransformation())) { */ private void deferStaticCompilation(ClassNode annotationSource, ClassNode target, SourceUnit source) { if (compilationUnit == null) { - applyStaticCompilation(annotationSource, target, source); - return; + applyStaticCompilation(annotationSource, target, source) + return } - compilationUnit.addPhaseOperation((CompilationUnit.ISourceUnitOperation) unit -> { + compilationUnit.addPhaseOperation(({ SourceUnit unit -> if (unit == source) { - applyStaticCompilation(annotationSource, target, source); + applyStaticCompilation(annotationSource, target, source) } - }, Phases.INSTRUCTION_SELECTION); + } as CompilationUnit.ISourceUnitOperation), Phases.INSTRUCTION_SELECTION) } private boolean applyStaticTypesTransformation(ClassNode pluginClass, ClassNode sibling, SourceUnit source, Class annotationType, StaticTypesTransformation transformation) { - List annotations = pluginClass.getAnnotations(ClassHelper.make(annotationType)); + List annotations = pluginClass.getAnnotations(ClassHelper.make(annotationType)) if (annotations.isEmpty()) { - return false; + return false } - AnnotationNode sourceAnnotation = annotations.get(0); - AnnotationNode siblingAnnotation = new AnnotationNode(ClassHelper.make(annotationType)); - sourceAnnotation.getMembers().forEach(siblingAnnotation::setMember); - siblingAnnotation.setSourcePosition(sourceAnnotation); - sibling.addAnnotation(siblingAnnotation); + AnnotationNode sourceAnnotation = annotations.get(0) + AnnotationNode siblingAnnotation = new AnnotationNode(ClassHelper.make(annotationType)) + sourceAnnotation.getMembers().forEach({ name, value -> siblingAnnotation.setMember(name, value) }) + siblingAnnotation.setSourcePosition(sourceAnnotation) + sibling.addAnnotation(siblingAnnotation) - transformation.setCompilationUnit(compilationUnit); - transformation.visit(new ASTNode[] { siblingAnnotation, sibling }, source); - return true; + transformation.setCompilationUnit(compilationUnit) + transformation.visit([siblingAnnotation, sibling] as ASTNode[], source) + return true } private List beanStatements(ClosureExpression dsl) { - Statement code = dsl.getCode(); + Statement code = dsl.getCode() if (code instanceof BlockStatement) { - return ((BlockStatement) code).getStatements(); + return ((BlockStatement) code).getStatements() } - List single = new ArrayList<>(); - single.add(code); - return single; + List single = new ArrayList<>() + single.add(code) + return single } // Generated names must not collide with anything the host class already has: its own fields @@ -776,38 +761,38 @@ private List beanStatements(ClosureExpression dsl) { // named 'toString' or after an interface's default method must synthesize, not override - // and the GroovyObject methods Groovy itself adds at class generation. private Set existingMemberNames(ClassNode host) { - Set names = new HashSet<>(); - for (FieldNode field : host.getFields()) { - names.add(field.getName()); + Set names = new HashSet<>() + for (FieldNode field in host.getFields()) { + names.add(field.getName()) } - Set visited = new HashSet<>(); - collectMethodNames(host, names, visited); - collectMethodNames(ClassHelper.GROOVY_OBJECT_TYPE, names, visited); - return names; + Set visited = new HashSet<>() + collectMethodNames(host, names, visited) + collectMethodNames(ClassHelper.GROOVY_OBJECT_TYPE, names, visited) + return names } private void collectMethodNames(ClassNode type, Set names, Set visited) { if (type == null || !visited.add(type.getName())) { - return; + return } - for (MethodNode method : type.getMethods()) { - names.add(method.getName()); + for (MethodNode method in type.getMethods()) { + names.add(method.getName()) } // A Groovy property's accessors are synthesized by the Verifier at class generation, AFTER // this transform runs, so they are not in getMethods() yet - reserve the names they will // occupy, or a same-named bean method would displace the real accessor. - for (PropertyNode property : type.getProperties()) { - String capitalized = BeanUtils.capitalize(property.getName()); - names.add("get" + capitalized); - names.add("set" + capitalized); + for (PropertyNode property in type.getProperties()) { + String capitalized = BeanUtils.capitalize(property.getName()) + names.add('get' + capitalized) + names.add('set' + capitalized) if (ClassHelper.boolean_TYPE.equals(property.getType()) || ClassHelper.Boolean_TYPE.equals(property.getType())) { - names.add("is" + capitalized); + names.add('is' + capitalized) } } - collectMethodNames(type.getSuperClass(), names, visited); - for (ClassNode implemented : type.getInterfaces()) { - collectMethodNames(implemented, names, visited); + collectMethodNames(type.getSuperClass(), names, visited) + for (ClassNode implemented in type.getInterfaces()) { + collectMethodNames(implemented, names, visited) } } @@ -817,14 +802,14 @@ private void collectMethodNames(ClassNode type, Set names, Set v private boolean isBeanRootedStatement(Statement statement) { if (!(statement instanceof ExpressionStatement) || !(((ExpressionStatement) statement).getExpression() instanceof MethodCallExpression)) { - return false; + return false } - MethodCallExpression call = (MethodCallExpression) ((ExpressionStatement) statement).getExpression(); + MethodCallExpression call = (MethodCallExpression) ((ExpressionStatement) statement).getExpression() while (!ROOT_STATEMENT_CALL_NAMES.contains(call.getMethodAsString()) && call.getObjectExpression() instanceof MethodCallExpression) { - call = (MethodCallExpression) call.getObjectExpression(); + call = (MethodCallExpression) call.getObjectExpression() } - return BEAN_CALL.equals(call.getMethodAsString()); + return BEAN_CALL.equals(call.getMethodAsString()) } // A Spring bean name may be declared by more than one bean(...) statement - the standard @@ -839,42 +824,42 @@ private boolean isBeanRootedStatement(Statement statement) { // never tell them apart. private void validateSharedBeanNames(List statements, ClassNode declaringClass, SourceUnit source) { - Map> usesByName = new LinkedHashMap<>(); - for (Statement statement : statements) { + Map> usesByName = new LinkedHashMap<>() + for (Statement statement in statements) { if (!isBeanRootedStatement(statement)) { - continue; + continue } BeanNameUse use = parseBeanNameUse(declaringClass, - (MethodCallExpression) ((ExpressionStatement) statement).getExpression()); + (MethodCallExpression) ((ExpressionStatement) statement).getExpression()) if (use != null) { - usesByName.computeIfAbsent(use.beanName, key -> new ArrayList<>()).add(use); + usesByName.computeIfAbsent(use.beanName, { key -> new ArrayList<>() }).add(use) } } - for (List uses : usesByName.values()) { + for (List uses in usesByName.values()) { if (uses.size() < 2) { - continue; + continue } - for (BeanNameUse use : uses) { + for (BeanNameUse use in uses) { if (!use.conditioned) { - addError(use.baseCall, source, "\"" + use.beanName + "\" is already used as the Spring " + - "bean name of another bean(...) statement - declaring it more than once is only " + - "allowed when every declaration with the name carries its own discriminating " + - "condition (e.g. .conditionalOnProperty(...), .conditionalOnBean(...), or " + - ".conditionalOnGrailsEnv(...)), so that at most one of them registers at runtime"); + addError(use.baseCall, source, '"' + use.beanName + '" is already used as the Spring ' + + 'bean name of another bean(...) statement - declaring it more than once is only ' + + 'allowed when every declaration with the name carries its own discriminating ' + + 'condition (e.g. .conditionalOnProperty(...), .conditionalOnBean(...), or ' + + '.conditionalOnGrailsEnv(...)), so that at most one of them registers at runtime') } } } } private static final class BeanNameUse { - private final String beanName; - private final MethodCallExpression baseCall; - private final boolean conditioned; + private final String beanName + private final MethodCallExpression baseCall + private final boolean conditioned BeanNameUse(String beanName, MethodCallExpression baseCall, boolean conditioned) { - this.beanName = beanName; - this.baseCall = baseCall; - this.conditioned = conditioned; + this.beanName = beanName + this.baseCall = baseCall + this.conditioned = conditioned } } @@ -885,49 +870,49 @@ private static final class BeanNameUse { // A trailing pair of type literals is "declared type, implementation". private boolean namesImplementation(List args) { return args.size() >= 2 && args.get(args.size() - 1) instanceof ClassExpression && - args.get(args.size() - 2) instanceof ClassExpression; + args.get(args.size() - 2) instanceof ClassExpression } private List withoutImplementation(List args) { - return args.subList(0, args.size() - 1); + return args.subList(0, args.size() - 1) } private BeanNameUse parseBeanNameUse(ClassNode declaringClass, MethodCallExpression outerCall) { - List qualifierCalls = new ArrayList<>(); - MethodCallExpression baseCall = outerCall; + List qualifierCalls = new ArrayList<>() + MethodCallExpression baseCall = outerCall while (!ROOT_STATEMENT_CALL_NAMES.contains(baseCall.getMethodAsString())) { if (!(baseCall.getObjectExpression() instanceof MethodCallExpression)) { - return null; + return null } - qualifierCalls.add(baseCall); - baseCall = (MethodCallExpression) baseCall.getObjectExpression(); + qualifierCalls.add(baseCall) + baseCall = (MethodCallExpression) baseCall.getObjectExpression() } if (!BEAN_CALL.equals(baseCall.getMethodAsString())) { - return null; + return null } // The same head rules processBeanStatement uses. Reading them separately is how the // implementation form and constant names fell out of this check while still compiling: a // three-argument declaration and a non-literal name both simply returned null here, so two // declarations of one name passed validation and Spring silently kept the first. - List args = withoutTrailingClosure(flatten(baseCall.getArguments()), baseCall, outerCall); + List args = withoutTrailingClosure(flatten(baseCall.getArguments()), baseCall, outerCall) if (namesImplementation(args)) { - args = withoutImplementation(args); + args = withoutImplementation(args) } if (args.isEmpty() || args.size() > 2 || !(args.get(args.size() - 1) instanceof ClassExpression)) { - return null; + return null } - String name; + String name if (args.size() == 1) { - name = decapitalize(simpleName(((ClassExpression) args.get(0)).getType())); + name = decapitalize(simpleName(((ClassExpression) args.get(0)).getType())) } else { - name = resolveStringConstant(args.get(0), declaringClass); + name = resolveStringConstant(args.get(0), declaringClass) if (name == null) { - return null; + return null } } - return new BeanNameUse(name, baseCall, hasDiscriminatingCondition(qualifierCalls, outerCall)); + return new BeanNameUse(name, baseCall, hasDiscriminatingCondition(qualifierCalls, outerCall)) } /** @@ -946,66 +931,66 @@ private BeanNameUse parseBeanNameUse(ClassNode declaringClass, MethodCallExpress */ private static final Set DISCRIMINATING_QUALIFIER_CALL_NAMES = Set.of(CONDITIONAL_ON_BEAN_CALL, CONDITIONAL_ON_PROPERTY_CALL, CONDITIONAL_ON_EXPRESSION_CALL, - CONDITIONAL_ON_CLASS_CALL, CONDITIONAL_ON_GRAILS_ENV_CALL); + CONDITIONAL_ON_CLASS_CALL, CONDITIONAL_ON_GRAILS_ENV_CALL) private boolean hasDiscriminatingCondition(List qualifierCalls, MethodCallExpression outerCall) { - for (MethodCallExpression qualifierCall : qualifierCalls) { - String qualifierName = qualifierCall.getMethodAsString(); - List args = withoutTrailingClosure(flatten(qualifierCall.getArguments()), qualifierCall, outerCall); + for (MethodCallExpression qualifierCall in qualifierCalls) { + String qualifierName = qualifierCall.getMethodAsString() + List args = withoutTrailingClosure(flatten(qualifierCall.getArguments()), qualifierCall, outerCall) if (CONDITIONAL_ON_MISSING_BEAN_CALL.equals(qualifierName) && discriminatesByType(args)) { - return true; + return true } if (DISCRIMINATING_QUALIFIER_CALL_NAMES.contains(qualifierName) && !args.isEmpty()) { - return true; + return true } if (ANNOTATE_CALL.equals(qualifierName)) { - for (Expression arg : args) { + for (Expression arg in args) { if (arg instanceof ClassExpression && isConditionalAnnotation(((ClassExpression) arg).getType())) { - return true; + return true } } } } - return false; + return false } // @ConditionalOnMissingBean attributes that say nothing about a *type*. Where duplicates share // one bean name - the only situation validateSharedBeanNames runs in - a name: or search: is // identical on each of them and so can no more tell them apart than the bare form can. - private static final Set NON_DISCRIMINATING_MISSING_BEAN_ATTRIBUTES = Set.of("name", "search"); + private static final Set NON_DISCRIMINATING_MISSING_BEAN_ATTRIBUTES = Set.of('name', 'search') private boolean discriminatesByType(List args) { - boolean sawSomething = false; - for (Expression arg : args) { + boolean sawSomething = false + for (Expression arg in args) { if (arg instanceof ClassExpression) { - return true; + return true } if (!(arg instanceof MapExpression)) { // an argument shape this method does not model - stay lenient rather than reject - return true; + return true } - for (MapEntryExpression entry : ((MapExpression) arg).getMapEntryExpressions()) { + for (MapEntryExpression entry in ((MapExpression) arg).getMapEntryExpressions()) { Object key = entry.getKeyExpression() instanceof ConstantExpression ? - ((ConstantExpression) entry.getKeyExpression()).getValue() : null; + ((ConstantExpression) entry.getKeyExpression()).getValue() : null if (!(key instanceof String) || !NON_DISCRIMINATING_MISSING_BEAN_ATTRIBUTES.contains(key)) { - return true; + return true } - sawSomething = true; + sawSomething = true } } - return !sawSomething && !args.isEmpty(); + return !sawSomething && !args.isEmpty() } private List withoutTrailingClosure(List args, MethodCallExpression call, MethodCallExpression outerCall) { if (call == outerCall && !args.isEmpty() && args.get(args.size() - 1) instanceof ClosureExpression) { - return args.subList(0, args.size() - 1); + return args.subList(0, args.size() - 1) } - return args; + return args } private boolean isConditionalAnnotation(ClassNode annotationType) { - return isConditionalAnnotation(annotationType, new HashSet<>()); + return isConditionalAnnotation(annotationType, new HashSet<>()) } // The same transitive meta-annotation walk belongsOnSibling does, against @Conditional alone: @@ -1013,104 +998,104 @@ private boolean isConditionalAnnotation(ClassNode annotationType) { // eventually reaches @Conditional through its meta-annotations. private boolean isConditionalAnnotation(ClassNode annotationType, Set visited) { if (!visited.add(annotationType.getName())) { - return false; + return false } - if (Conditional.class.getName().equals(annotationType.getName())) { - return true; + if (Conditional.getName().equals(annotationType.getName())) { + return true } - for (AnnotationNode metaAnnotation : annotationType.getAnnotations()) { + for (AnnotationNode metaAnnotation in annotationType.getAnnotations()) { if (isConditionalAnnotation(metaAnnotation.getClassNode(), visited)) { - return true; + return true } } - return false; + return false } private void processStatement(ClassNode classNode, ClassNode declaringClass, Statement statement, SourceUnit source, Set usedNames) { if (!(statement instanceof ExpressionStatement) || !(((ExpressionStatement) statement).getExpression() instanceof MethodCallExpression)) { - addError(statement, source, "Each 'beans' statement must be a bean(...), field(...), or method(...) call"); - return; + addError(statement, source, "Each 'beans' statement must be a bean(...), field(...), or method(...) call") + return } - MethodCallExpression outerCall = (MethodCallExpression) ((ExpressionStatement) statement).getExpression(); + MethodCallExpression outerCall = (MethodCallExpression) ((ExpressionStatement) statement).getExpression() // Walk from the outermost (last-written) call back to the bean(...)/field(...)/method(...) // call at the root, collecting any chained qualifiers along the way. - List qualifierCalls = new ArrayList<>(); - MethodCallExpression baseCall = outerCall; + List qualifierCalls = new ArrayList<>() + MethodCallExpression baseCall = outerCall while (!ROOT_STATEMENT_CALL_NAMES.contains(baseCall.getMethodAsString())) { if (!ALL_QUALIFIER_CALL_NAMES.contains(baseCall.getMethodAsString()) || !(baseCall.getObjectExpression() instanceof MethodCallExpression)) { - addError(statement, source, "Expected bean([\"name\", ] Type) { ... }, field([\"name\", ] Type), " + - "or method([\"name\", ] Type) { ... }, optionally chained with qualifiers"); - return; + addError(statement, source, 'Expected bean(["name", ] Type) { ... }, field(["name", ] Type), ' + + 'or method(["name", ] Type) { ... }, optionally chained with qualifiers') + return } - qualifierCalls.add(0, baseCall); - baseCall = (MethodCallExpression) baseCall.getObjectExpression(); + qualifierCalls.add(0, baseCall) + baseCall = (MethodCallExpression) baseCall.getObjectExpression() } // The walk above stops at the first root call name it meets, so without this a root // statement chained onto another - field("suffix", String).bean("greeter", String) { } - // parses as one statement and the left-hand declaration is silently dropped. if (!baseCall.isImplicitThis() && baseCall.getObjectExpression() instanceof MethodCallExpression) { - addError(statement, source, baseCall.getMethodAsString() + "(...) cannot be chained onto " + + addError(statement, source, baseCall.getMethodAsString() + '(...) cannot be chained onto ' + ((MethodCallExpression) baseCall.getObjectExpression()).getMethodAsString() + - "(...) - each bean(...), field(...) and method(...) declaration is its own statement"); - return; + '(...) - each bean(...), field(...) and method(...) declaration is its own statement') + return } - String rootName = baseCall.getMethodAsString(); + String rootName = baseCall.getMethodAsString() // The body closure belongs to the outermost call, after every qualifier. Written the other // way round it sits on the root call instead, where the [name, ] Type parsing below finds it // as an extra argument and reports something unrelated - that the name and type are the wrong // way around, or that a type is missing. - List rootArgs = flatten(baseCall.getArguments()); + List rootArgs = flatten(baseCall.getArguments()) if (!qualifierCalls.isEmpty() && !rootArgs.isEmpty() && rootArgs.get(rootArgs.size() - 1) instanceof ClosureExpression) { - String firstQualifier = qualifierCalls.get(0).getMethodAsString(); - addError(statement, source, "the body closure comes last, after every chained qualifier - " + - "write " + rootName + "(...)." + firstQualifier + "(...) { ... } rather than " + - rootName + "(...) { ... }." + firstQualifier + "(...)"); - return; + String firstQualifier = qualifierCalls.get(0).getMethodAsString() + addError(statement, source, 'the body closure comes last, after every chained qualifier - ' + + 'write ' + rootName + '(...).' + firstQualifier + '(...) { ... } rather than ' + + rootName + '(...) { ... }.' + firstQualifier + '(...)') + return } - boolean isBean = BEAN_CALL.equals(rootName); + boolean isBean = BEAN_CALL.equals(rootName) Set allowedQualifiers = isBean ? BEAN_QUALIFIER_CALL_NAMES : FIELD_CALL.equals(rootName) ? FIELD_QUALIFIER_CALL_NAMES : - GROUP_CALL.equals(rootName) ? GROUP_QUALIFIER_CALL_NAMES : METHOD_QUALIFIER_CALL_NAMES; - for (MethodCallExpression qualifierCall : qualifierCalls) { + GROUP_CALL.equals(rootName) ? GROUP_QUALIFIER_CALL_NAMES : METHOD_QUALIFIER_CALL_NAMES + for (MethodCallExpression qualifierCall in qualifierCalls) { if (!allowedQualifiers.contains(qualifierCall.getMethodAsString())) { - addError(qualifierCall, source, "." + qualifierCall.getMethodAsString() + "(...) cannot be " + - "chained onto " + rootName + "(...)"); - return; + addError(qualifierCall, source, '.' + qualifierCall.getMethodAsString() + '(...) cannot be ' + + 'chained onto ' + rootName + '(...)') + return } } // .annotate(...) is repeatable (once per distinct annotation type, enforced when the // annotation is actually attached below); every other qualifier is single-use. - Set seenQualifiers = new HashSet<>(); - for (MethodCallExpression qualifierCall : qualifierCalls) { - String qualifierName = qualifierCall.getMethodAsString(); + Set seenQualifiers = new HashSet<>() + for (MethodCallExpression qualifierCall in qualifierCalls) { + String qualifierName = qualifierCall.getMethodAsString() if (!ANNOTATE_CALL.equals(qualifierName) && !seenQualifiers.add(qualifierName)) { - addError(qualifierCall, source, "." + qualifierName + "(...) may only be chained once per " + - rootName + "(...)"); - return; + addError(qualifierCall, source, '.' + qualifierName + '(...) may only be chained once per ' + + rootName + '(...)') + return } } if (isBean) { - processBeanStatement(classNode, declaringClass, outerCall, baseCall, qualifierCalls, source, usedNames); + processBeanStatement(classNode, declaringClass, outerCall, baseCall, qualifierCalls, source, usedNames) } else if (GROUP_CALL.equals(rootName)) { - processGroupStatement(classNode, declaringClass, outerCall, baseCall, qualifierCalls, source, usedNames); + processGroupStatement(classNode, declaringClass, outerCall, baseCall, qualifierCalls, source, usedNames) } else if (FIELD_CALL.equals(rootName)) { - processFieldStatement(classNode, declaringClass, baseCall, qualifierCalls, source, usedNames); + processFieldStatement(classNode, declaringClass, baseCall, qualifierCalls, source, usedNames) } else { - processMethodStatement(classNode, declaringClass, outerCall, baseCall, qualifierCalls, source, usedNames); + processMethodStatement(classNode, declaringClass, outerCall, baseCall, qualifierCalls, source, usedNames) } } @@ -1133,98 +1118,98 @@ else if (FIELD_CALL.equals(rootName)) { private void processGroupStatement(ClassNode classNode, ClassNode declaringClass, MethodCallExpression outerCall, MethodCallExpression baseCall, List qualifierCalls, SourceUnit source, Set usedNames) { - List closureCallArgs = flatten(outerCall.getArguments()); + List closureCallArgs = flatten(outerCall.getArguments()) if (closureCallArgs.isEmpty() || !(closureCallArgs.get(closureCallArgs.size() - 1) instanceof ClosureExpression)) { - addError(outerCall, source, "group(...) must end with a body closure: group(\"name\") { ... }"); - return; + addError(outerCall, source, 'group(...) must end with a body closure: group("name") { ... }') + return } - ClosureExpression body = (ClosureExpression) closureCallArgs.get(closureCallArgs.size() - 1); + ClosureExpression body = (ClosureExpression) closureCallArgs.get(closureCallArgs.size() - 1) if (body.getParameters() != null && body.getParameters().length > 0) { - addError(outerCall, source, "group(...) takes no closure parameters - a group declares a class, " + - "not a bean, so there is nothing to inject into. Put the parameters on the bean(...) " + - "declarations inside it"); - return; + addError(outerCall, source, 'group(...) takes no closure parameters - a group declares a class, ' + + 'not a bean, so there is nothing to inject into. Put the parameters on the bean(...) ' + + 'declarations inside it') + return } - List baseArgs = flatten(baseCall.getArguments()); + List baseArgs = flatten(baseCall.getArguments()) if (baseCall == outerCall && !baseArgs.isEmpty()) { - baseArgs = baseArgs.subList(0, baseArgs.size() - 1); + baseArgs = baseArgs.subList(0, baseArgs.size() - 1) } if (baseArgs.size() != 1) { - addError(baseCall, source, "group(...) takes a name, e.g. group(\"imageServing\") { ... }"); - return; + addError(baseCall, source, 'group(...) takes a name, e.g. group("imageServing") { ... }') + return } - String name = resolveStringConstant(baseArgs.get(0), declaringClass); + String name = resolveStringConstant(baseArgs.get(0), declaringClass) if (name == null || !isValidJavaIdentifier(BeanUtils.capitalize(name))) { - addError(baseArgs.get(0), source, "group(name) requires the name to be a String literal or a " + - "compile-time String constant that is a valid Java identifier - it becomes the nested " + - "class's name, e.g. group(\"imageServing\")"); - return; + addError(baseArgs.get(0), source, 'group(name) requires the name to be a String literal or a ' + + 'compile-time String constant that is a valid Java identifier - it becomes the nested ' + + "class's name, e.g. group(\"imageServing\")") + return } // JacksonObjectMapperConfiguration rather than JacksonObjectMapper: the suffix is what says // this is a configuration class when it turns up in a stack trace or /actuator/beans. - String simpleName = BeanUtils.capitalize(name); - if (!simpleName.endsWith("Configuration")) { - simpleName = simpleName + "Configuration"; + String simpleName = BeanUtils.capitalize(name) + if (!simpleName.endsWith('Configuration')) { + simpleName = simpleName + 'Configuration' } if (!registerName(simpleName, baseCall, source, usedNames, - "is already used by another member of the class - generated member names must be unique")) { - return; + 'is already used by another member of the class - generated member names must be unique')) { + return } - List statements = beanStatements(body); + List statements = beanStatements(body) if (statements.isEmpty()) { - addError(outerCall, source, "group(\"" + name + "\") declares nothing - a group exists to put a " + - "condition on the declarations inside it"); - return; + addError(outerCall, source, 'group("' + name + '") declares nothing - a group exists to put a ' + + 'condition on the declarations inside it') + return } - for (Statement statement : statements) { + for (Statement statement in statements) { if (isGroupRootedStatement(statement)) { - addError(statement, source, "group(...) cannot be nested - flatten it, or give the inner " + - "group its own conditions at the top level"); - return; + addError(statement, source, 'group(...) cannot be nested - flatten it, or give the inner ' + + 'group its own conditions at the top level') + return } } - InnerClassNode group = new InnerClassNode(classNode, classNode.getName() + "$" + simpleName, - Modifier.PUBLIC | Modifier.STATIC, ClassHelper.OBJECT_TYPE); - group.setSourcePosition(baseCall); - source.getAST().addClass(group); + InnerClassNode group = new InnerClassNode(classNode, classNode.getName() + '\$' + simpleName, + Modifier.PUBLIC | Modifier.STATIC, ClassHelper.OBJECT_TYPE) + group.setSourcePosition(baseCall) + source.getAST().addClass(group) // proxyBeanMethods = false, matching what Spring Boot's own nested configuration classes // carry - and keeping the sibling-call check below meaningful inside the group. - AnnotationNode configuration = new AnnotationNode(ClassHelper.make(Configuration.class)); - configuration.setMember(PROXY_BEAN_METHODS_MEMBER, new ConstantExpression(Boolean.FALSE)); - group.addAnnotation(withPosition(configuration, baseCall)); + AnnotationNode configuration = new AnnotationNode(ClassHelper.make(Configuration)) + configuration.setMember(PROXY_BEAN_METHODS_MEMBER, new ConstantExpression(Boolean.FALSE)) + group.addAnnotation(withPosition(configuration, baseCall)) - for (MethodCallExpression qualifierCall : qualifierCalls) { - List qualifierArgs = flatten(qualifierCall.getArguments()); + for (MethodCallExpression qualifierCall in qualifierCalls) { + List qualifierArgs = flatten(qualifierCall.getArguments()) if (qualifierCall == outerCall) { - qualifierArgs = qualifierArgs.subList(0, qualifierArgs.size() - 1); + qualifierArgs = qualifierArgs.subList(0, qualifierArgs.size() - 1) } if (!applyGroupQualifier(group, qualifierCall, qualifierArgs, source)) { - return; + return } } - Set groupNames = existingMemberNames(group); - List preExisting = new ArrayList<>(group.getMethods()); - validateSharedBeanNames(statements, declaringClass, source); - for (Statement statement : statements) { + Set groupNames = existingMemberNames(group) + List preExisting = new ArrayList<>(group.getMethods()) + validateSharedBeanNames(statements, declaringClass, source) + for (Statement statement in statements) { if (!isBeanRootedStatement(statement)) { - processStatement(group, declaringClass, statement, source, groupNames); + processStatement(group, declaringClass, statement, source, groupNames) } } - for (Statement statement : statements) { + for (Statement statement in statements) { if (isBeanRootedStatement(statement)) { - processStatement(group, declaringClass, statement, source, groupNames); + processStatement(group, declaringClass, statement, source, groupNames) } } - List generated = generatedMembers(group, preExisting); - rejectUnproxiedSiblingBeanCalls(group, generated, source); - rejectAnonymousClassReachingOutward(group, true, generated, source); - dumpGeneratedMembers(group, generated, new ArrayList<>(group.getFields()), source); + List generated = generatedMembers(group, preExisting) + rejectUnproxiedSiblingBeanCalls(group, generated, source) + rejectAnonymousClassReachingOutward(group, true, generated, source) + dumpGeneratedMembers(group, generated, new ArrayList<>(group.getFields()), source) // The group is compiled as its own class, so it needs the host's static-compilation // treatment in its own right - otherwise its bodies are dynamic inside a @CompileStatic file. @@ -1236,58 +1221,58 @@ private void processGroupStatement(ClassNode classNode, ClassNode declaringClass // type-checked, and class generation dies with "StaticTypesCallSiteWriter#makeCallSite // should not have been called". Waiting until the methods exist is what a hand-written // annotation effectively gets. - deferStaticCompilation(classNode, group, source); + deferStaticCompilation(classNode, group, source) } // Same silent classification as isBeanRootedStatement, for the nesting check. private boolean isGroupRootedStatement(Statement statement) { if (!(statement instanceof ExpressionStatement) || !(((ExpressionStatement) statement).getExpression() instanceof MethodCallExpression)) { - return false; + return false } - MethodCallExpression call = (MethodCallExpression) ((ExpressionStatement) statement).getExpression(); + MethodCallExpression call = (MethodCallExpression) ((ExpressionStatement) statement).getExpression() while (!ROOT_STATEMENT_CALL_NAMES.contains(call.getMethodAsString()) && call.getObjectExpression() instanceof MethodCallExpression) { - call = (MethodCallExpression) call.getObjectExpression(); + call = (MethodCallExpression) call.getObjectExpression() } - return GROUP_CALL.equals(call.getMethodAsString()); + return GROUP_CALL.equals(call.getMethodAsString()) } private boolean applyGroupQualifier(ClassNode group, MethodCallExpression qualifierCall, List args, SourceUnit source) { - String name = qualifierCall.getMethodAsString(); - AnnotationNode annotation; + String name = qualifierCall.getMethodAsString() + AnnotationNode annotation if (CONDITIONAL_ON_BEAN_CALL.equals(name)) { - annotation = conditionalOnBeanAnnotation(args, qualifierCall, source); + annotation = conditionalOnBeanAnnotation(args, qualifierCall, source) } else if (CONDITIONAL_ON_MISSING_BEAN_CALL.equals(name)) { - annotation = conditionalOnMissingBeanAnnotation(args, qualifierCall, source); + annotation = conditionalOnMissingBeanAnnotation(args, qualifierCall, source) } else if (CONDITIONAL_ON_PROPERTY_CALL.equals(name)) { - annotation = conditionalOnPropertyAnnotation(args, qualifierCall, source); + annotation = conditionalOnPropertyAnnotation(args, qualifierCall, source) } else if (CONDITIONAL_ON_EXPRESSION_CALL.equals(name)) { - annotation = conditionalOnExpressionAnnotation(args, qualifierCall, source); + annotation = conditionalOnExpressionAnnotation(args, qualifierCall, source) } else if (CONDITIONAL_ON_CLASS_CALL.equals(name)) { - annotation = conditionalOnClassAnnotation(args, qualifierCall, source); + annotation = conditionalOnClassAnnotation(args, qualifierCall, source) } else if (CONDITIONAL_ON_GRAILS_ENV_CALL.equals(name)) { // attaches directly rather than returning a node, like the qualifier path it shares - return applyConditionalOnGrailsEnvQualifier(group, qualifierCall, args, source); + return applyConditionalOnGrailsEnvQualifier(group, qualifierCall, args, source) } else { - return applyGenericAnnotation(group, qualifierCall, args, source); + return applyGenericAnnotation(group, qualifierCall, args, source) } - return annotation != null && addAnnotationIfAbsent(group, qualifierCall, annotation, source); + return annotation != null && addAnnotationIfAbsent(group, qualifierCall, annotation, source) } private boolean registerName(String name, ASTNode location, SourceUnit source, Set usedNames, String errorSuffix) { if (!usedNames.add(name)) { - addError(location, source, "\"" + name + "\" " + errorSuffix); - return false; + addError(location, source, '"' + name + '" ' + errorSuffix) + return false } - return true; + return true } private void processBeanStatement(ClassNode classNode, ClassNode declaringClass, @@ -1296,77 +1281,77 @@ private void processBeanStatement(ClassNode classNode, ClassNode declaringClass, // The factory closure is optional: bean(Type) with no body declares a bean that is just its // own no-argument construction, which is by far the most common shape and reads as noise // when spelled out as bean(Type) { new Type() }. - List closureCallArgs = flatten(outerCall.getArguments()); + List closureCallArgs = flatten(outerCall.getArguments()) ClosureExpression factory = !closureCallArgs.isEmpty() && closureCallArgs.get(closureCallArgs.size() - 1) instanceof ClosureExpression ? - (ClosureExpression) closureCallArgs.get(closureCallArgs.size() - 1) : null; + (ClosureExpression) closureCallArgs.get(closureCallArgs.size() - 1) : null // When bean(...) is itself the outermost call (no qualifiers chained), it carries the // trailing closure as its own last argument - exclude it before validating the [name, ] Type // shape, since it was already validated above. - List baseArgs = flatten(baseCall.getArguments()); + List baseArgs = flatten(baseCall.getArguments()) if (factory != null && baseCall == outerCall && !baseArgs.isEmpty()) { - baseArgs = baseArgs.subList(0, baseArgs.size() - 1); + baseArgs = baseArgs.subList(0, baseArgs.size() - 1) } - TypeAndName typeAndName = parseNameAndType(baseArgs, baseCall, source, BEAN_CALL, false, true, declaringClass); + TypeAndName typeAndName = parseNameAndType(baseArgs, baseCall, source, BEAN_CALL, false, true, declaringClass) if (typeAndName == null) { - return; + return } - ClassNode beanType = declaredType(typeAndName, qualifierCalls, outerCall, factory != null, source, BEAN_CALL); + ClassNode beanType = declaredType(typeAndName, qualifierCalls, outerCall, factory != null, source, BEAN_CALL) if (beanType == null) { - return; + return } - ClassNode implementationType = typeAndName.implementation == null ? null : typeAndName.implementation.getType(); - String declaredName = typeAndName.type.getType().getNameWithoutPackage(); + ClassNode implementationType = typeAndName.implementation == null ? null : typeAndName.implementation.getType() + String declaredName = typeAndName.type.getType().getNameWithoutPackage() // A closure whose body is empty declares construction too, from its own parameters: the // parameters say what is injected, and the generated body is the constructor call the author // would otherwise have written out. bean(Type) { } with no parameters is bean(Type). - boolean constructsBean = factory == null || isEmpty(factory.getCode()); + boolean constructsBean = factory == null || isEmpty(factory.getCode()) if (implementationType != null) { - String implementationName = implementationType.getNameWithoutPackage(); + String implementationName = implementationType.getNameWithoutPackage() // Naming the implementation IS the construction, so a body answering the same question // again can only disagree with it. if (!constructsBean) { - addError(baseCall, source, "bean(" + declaredName + ", " + implementationName + ") already " + - "declares what to construct, so it takes no factory closure body - drop the body, or " + - "drop " + implementationName + " and construct it there"); - return; + addError(baseCall, source, 'bean(' + declaredName + ', ' + implementationName + ') already ' + + 'declares what to construct, so it takes no factory closure body - drop the body, or ' + + 'drop ' + implementationName + ' and construct it there') + return } if (!isSubtypeOf(implementationType, typeAndName.type.getType())) { - addError(baseCall, source, implementationName + " is not a " + declaredName + ", so it cannot " + - "be the implementation of a bean declared as " + declaredName); - return; + addError(baseCall, source, implementationName + ' is not a ' + declaredName + ', so it cannot ' + + 'be the implementation of a bean declared as ' + declaredName) + return } } // What the generated body actually calls new on: the implementation when one was named, // the declared type otherwise. - ClassNode constructedType = implementationType != null ? implementationType : beanType; + ClassNode constructedType = implementationType != null ? implementationType : beanType // The construction can already prove the declared type's type arguments - a bean declared as // AuditorAware and built from a SpringSecurityAuditorAware is an AuditorAware, and // Spring matches injection points against exactly that. Restating it in .typeArguments(...) // is then only an opportunity to state it differently from the truth. if (!hasExplicitTypeArguments(qualifierCalls)) { - ClassNode evidence = implementationType != null ? implementationType : constructedTypeFromBody(factory); - ClassNode inferred = inferTypeArguments(typeAndName.type.getType(), evidence); + ClassNode evidence = implementationType != null ? implementationType : constructedTypeFromBody(factory) + ClassNode inferred = inferTypeArguments(typeAndName.type.getType(), evidence) if (inferred != null) { - beanType = inferred; + beanType = inferred } } if (constructsBean && (constructedType.isInterface() || Modifier.isAbstract(constructedType.getModifiers()))) { if (implementationType != null) { - addError(baseCall, source, constructedType.getNameWithoutPackage() + " is an interface or abstract " + - "class, so it cannot be the implementation - name a concrete type: bean(" + declaredName + - ", SomeImplementation)"); + addError(baseCall, source, constructedType.getNameWithoutPackage() + ' is an interface or abstract ' + + 'class, so it cannot be the implementation - name a concrete type: bean(' + declaredName + + ', SomeImplementation)') } else { - addError(baseCall, source, "bean(" + declaredName + ") with no factory closure body " + - "constructs the declared type, which cannot be done for an interface or abstract class - " + - "name the implementation: bean(" + declaredName + ", SomeImplementation), or give it a " + - "body: bean(" + declaredName + ") { new SomeImplementation() }"); + addError(baseCall, source, 'bean(' + declaredName + ') with no factory closure body ' + + 'constructs the declared type, which cannot be done for an interface or abstract class - ' + + 'name the implementation: bean(' + declaredName + ', SomeImplementation), or give it a ' + + 'body: bean(' + declaredName + ') { new SomeImplementation() }') } - return; + return } // The method name is an implementation detail - Spring resolves the bean by its @@ -1375,14 +1360,14 @@ private void processBeanStatement(ClassNode classNode, ClassNode declaringClass, // of erroring. String javaMethodName = isValidJavaIdentifier(typeAndName.name) && !usedNames.contains(typeAndName.name) ? typeAndName.name : - syntheticBeanMethodName(typeAndName.type.getType(), usedNames); - usedNames.add(javaMethodName); + syntheticBeanMethodName(typeAndName.type.getType(), usedNames) + usedNames.add(javaMethodName) Parameter[] beanParameters = factory == null || factory.getParameters() == null ? - Parameter.EMPTY_ARRAY : factory.getParameters(); + Parameter.EMPTY_ARRAY : factory.getParameters() Statement beanBody = constructsBean ? synthesizedConstruction(constructedType, beanParameters, baseCall) : - factory.getCode(); + factory.getCode() MethodNode beanMethod = new MethodNode( javaMethodName, @@ -1390,35 +1375,35 @@ private void processBeanStatement(ClassNode classNode, ClassNode declaringClass, beanType, beanParameters, ClassNode.EMPTY_ARRAY, - beanBody); - beanMethod.setSourcePosition(baseCall); - beanMethod.addAnnotation(withPosition(beanAnnotation(typeAndName.name), baseCall)); + beanBody) + beanMethod.setSourcePosition(baseCall) + beanMethod.addAnnotation(withPosition(beanAnnotation(typeAndName.name), baseCall)) - for (MethodCallExpression qualifierCall : qualifierCalls) { - List qualifierArgs = flatten(qualifierCall.getArguments()); + for (MethodCallExpression qualifierCall in qualifierCalls) { + List qualifierArgs = flatten(qualifierCall.getArguments()) if (factory != null && qualifierCall == outerCall) { // only the outermost call in the chain can carry the trailing factory closure - qualifierArgs = qualifierArgs.subList(0, qualifierArgs.size() - 1); + qualifierArgs = qualifierArgs.subList(0, qualifierArgs.size() - 1) } if (!applyQualifier(beanMethod, typeAndName.name, qualifierCall, qualifierArgs, source)) { - return; + return } } if (!rejectNonStaticPostProcessor(beanMethod, beanType, baseCall, source)) { - return; + return } if (!rehomeAnonymousInnerClasses(beanMethod, classNode, Modifier.isStatic(beanMethod.getModifiers()), typeAndName.name, source)) { - return; + return } - classNode.addMethod(beanMethod); + classNode.addMethod(beanMethod) } - private static final String BEAN_FACTORY_POST_PROCESSOR = "org.springframework.beans.factory.config.BeanFactoryPostProcessor"; - private static final String BEAN_POST_PROCESSOR = "org.springframework.beans.factory.config.BeanPostProcessor"; + private static final String BEAN_FACTORY_POST_PROCESSOR = 'org.springframework.beans.factory.config.BeanFactoryPostProcessor' + private static final String BEAN_POST_PROCESSOR = 'org.springframework.beans.factory.config.BeanPostProcessor' /** * A {@code BeanFactoryPostProcessor}/{@code BeanPostProcessor} bean must be creatable without @@ -1436,23 +1421,23 @@ private void processBeanStatement(ClassNode classNode, ClassNode declaringClass, private boolean rejectNonStaticPostProcessor(MethodNode beanMethod, ClassNode beanType, ASTNode location, SourceUnit source) { if (Modifier.isStatic(beanMethod.getModifiers())) { - return true; + return true } - String postProcessorType = null; + String postProcessorType = null if (isSubtypeOf(beanType, ClassHelper.make(BEAN_FACTORY_POST_PROCESSOR))) { - postProcessorType = "BeanFactoryPostProcessor"; + postProcessorType = 'BeanFactoryPostProcessor' } else if (isSubtypeOf(beanType, ClassHelper.make(BEAN_POST_PROCESSOR))) { - postProcessorType = "BeanPostProcessor"; + postProcessorType = 'BeanPostProcessor' } if (postProcessorType == null) { - return true; + return true } - addError(location, source, "a " + postProcessorType + " bean must be declared " + - ".staticMethod(), so Spring can obtain it without instantiating this class - as an " + - "instance method it forces that instantiation before the beans it post-processes " + - "are configured"); - return false; + addError(location, source, 'a ' + postProcessorType + ' bean must be declared ' + + '.staticMethod(), so Spring can obtain it without instantiating this class - as an ' + + 'instance method it forces that instantiation before the beans it post-processes ' + + 'are configured') + return false } /** @@ -1475,93 +1460,93 @@ else if (isSubtypeOf(beanType, ClassHelper.make(BEAN_POST_PROCESSOR))) { */ private void dumpGeneratedMembers(ClassNode host, List methods, List fields, SourceUnit source) { - String dir = System.getProperty(DUMP_DIR_PROPERTY); + String dir = System.getProperty(DUMP_DIR_PROPERTY) if (dir == null || dir.isBlank()) { - return; + return } - StringBuilder text = new StringBuilder(); - text.append("// Generated from the 'beans' DSL in ").append(host.getName()).append('\n'); - text.append("// Bodies are omitted: each is the closure body from that source, lifted verbatim.\n"); - for (FieldNode field : fields) { - text.append('\n'); - for (AnnotationNode annotation : field.getAnnotations()) { - text.append(annotationText(annotation)).append('\n'); + StringBuilder text = new StringBuilder() + text.append("// Generated from the 'beans' DSL in ").append(host.getName()).append('\n') + text.append('// Bodies are omitted: each is the closure body from that source, lifted verbatim.\n') + for (FieldNode field in fields) { + text.append('\n') + for (AnnotationNode annotation in field.getAnnotations()) { + text.append(annotationText(annotation)).append('\n') } text.append(AstToTextHelper.getModifiersText(field.getModifiers())).append(' ') - .append(typeText(field.getType())).append(' ').append(field.getName()).append('\n'); + .append(typeText(field.getType())).append(' ').append(field.getName()).append('\n') } - for (MethodNode method : methods) { - text.append('\n'); - for (AnnotationNode annotation : method.getAnnotations()) { - text.append(annotationText(annotation)).append('\n'); + for (MethodNode method in methods) { + text.append('\n') + for (AnnotationNode annotation in method.getAnnotations()) { + text.append(annotationText(annotation)).append('\n') } text.append(AstToTextHelper.getModifiersText(method.getModifiers())).append(' ') .append(typeText(method.getReturnType())).append(' ').append(method.getName()) - .append('(').append(parametersText(method.getParameters())).append(")\n"); + .append('(').append(parametersText(method.getParameters())).append(')\n') } try { - Path target = Paths.get(dir); - Files.createDirectories(target); - Files.writeString(target.resolve(host.getName() + ".beans.txt"), text.toString(), - StandardCharsets.UTF_8); + Path target = Paths.get(dir) + Files.createDirectories(target) + Files.writeString(target.resolve(host.getName() + '.beans.txt'), text.toString(), + StandardCharsets.UTF_8) } catch (IOException | RuntimeException e) { // Opt-in by definition, so this can only fire for someone who asked for the dump and // would otherwise be left looking for a file that was never written. - addError(host, source, "could not write the beans DSL dump for " + host.getName() + " to \"" + - dir + "\" (" + DUMP_DIR_PROPERTY + "): " + e); + addError(host, source, 'could not write the beans DSL dump for ' + host.getName() + ' to "' + + dir + '" (' + DUMP_DIR_PROPERTY + '): ' + e) } } private String parametersText(Parameter[] parameters) { - StringBuilder text = new StringBuilder(); - for (Parameter parameter : parameters) { + StringBuilder text = new StringBuilder() + for (Parameter parameter in parameters) { if (text.length() > 0) { - text.append(", "); + text.append(', ') } - for (AnnotationNode annotation : parameter.getAnnotations()) { - text.append(annotationText(annotation)).append(' '); + for (AnnotationNode annotation in parameter.getAnnotations()) { + text.append(annotationText(annotation)).append(' ') } - text.append(typeText(parameter.getType())).append(' ').append(parameter.getName()); + text.append(typeText(parameter.getType())).append(' ').append(parameter.getName()) } - return text.toString(); + return text.toString() } // Type arguments are printed only when every one of them is concrete. A raw declared type // resolved from a class still reports its own type PARAMETERS here, and printing those would // read as when nothing of the sort was declared. private String typeText(ClassNode type) { - GenericsType[] generics = type.getGenericsTypes(); + GenericsType[] generics = type.getGenericsTypes() if (generics == null || generics.length == 0) { - return type.getName(); + return type.getName() } - StringBuilder text = new StringBuilder(type.getName()); - for (GenericsType generic : generics) { + StringBuilder text = new StringBuilder(type.getName()) + for (GenericsType generic in generics) { if (generic.isPlaceholder() || generic.isWildcard()) { - return type.getName(); + return type.getName() } } - text.append('<'); + text.append('<') for (int i = 0; i < generics.length; i++) { - text.append(i == 0 ? "" : ", ").append(generics[i].getType().getName()); + text.append(i == 0 ? '' : ', ').append(generics[i].getType().getName()) } - return text.append('>').toString(); + return text.append('>').toString() } private String annotationText(AnnotationNode annotation) { - StringBuilder text = new StringBuilder("@").append(annotation.getClassNode().getNameWithoutPackage()); - Map members = annotation.getMembers(); + StringBuilder text = new StringBuilder('@').append(annotation.getClassNode().getNameWithoutPackage()) + Map members = annotation.getMembers() if (members.isEmpty()) { - return text.toString(); + return text.toString() } - text.append('('); - boolean first = true; - for (Map.Entry member : members.entrySet()) { - text.append(first ? "" : ", ").append(member.getKey()).append(" = ") - .append(memberValueText(member.getValue())); - first = false; + text.append('(') + boolean first = true + for (Map.Entry member in members.entrySet()) { + text.append(first ? '' : ', ').append(member.getKey()).append(' = ') + .append(memberValueText(member.getValue())) + first = false } - return text.append(')').toString(); + return text.append(')').toString() } // Expression.getText() renders a String constant bare, so @DependsOn("names") would print as @@ -1569,26 +1554,26 @@ private String annotationText(AnnotationNode annotation) { // array-valued attribute reads the way it was written. private String memberValueText(Expression value) { if (value instanceof ConstantExpression && ((ConstantExpression) value).getValue() instanceof String) { - return "\"" + ((ConstantExpression) value).getValue() + "\""; + return '"' + ((ConstantExpression) value).getValue() + '"' } if (value instanceof ListExpression) { - StringBuilder text = new StringBuilder("["); - List entries = ((ListExpression) value).getExpressions(); + StringBuilder text = new StringBuilder('[') + List entries = ((ListExpression) value).getExpressions() for (int i = 0; i < entries.size(); i++) { - text.append(i == 0 ? "" : ", ").append(memberValueText(entries.get(i))); + text.append(i == 0 ? '' : ', ').append(memberValueText(entries.get(i))) } - return text.append(']').toString(); + return text.append(']').toString() } - return value.getText(); + return value.getText() } // The methods this block just generated, in declaration order: everything on the host that was // not there before the two processing loops ran. MethodNode does not override equals, so the // removal is by identity and cannot drop a same-signature method the user wrote. private List generatedMembers(ClassNode host, List preExisting) { - List generated = new ArrayList<>(host.getMethods()); - generated.removeAll(preExisting); - return generated; + List generated = new ArrayList<>(host.getMethods()) + generated.removeAll(preExisting) + return generated } /** @@ -1615,33 +1600,33 @@ private List generatedMembers(ClassNode host, List preEx * DSL silently changes what those calls mean.

*/ private void rejectUnproxiedSiblingBeanCalls(ClassNode host, List generated, SourceUnit source) { - boolean proxied = beanMethodsAreProxied(host); - Map beanMethodsByName = new LinkedHashMap<>(); + boolean proxied = beanMethodsAreProxied(host) + Map beanMethodsByName = new LinkedHashMap<>() // Every @Bean method on the host, not only the ones this block generated. A class that // mixes hand-written @Bean methods with the DSL is what a migration looks like midway // through, and a call to one of those from a generated body misses the singleton in exactly // the same way - more easily, in fact, since it was correct in the @Configuration class the // beans are being moved out of. Only generated bodies are scanned: what a hand-written // method does is its author's business, not this transform's. - for (MethodNode method : host.getMethods()) { - if (method.getAnnotations(ClassHelper.make(Bean.class)).isEmpty()) { - continue; + for (MethodNode method in host.getMethods()) { + if (method.getAnnotations(ClassHelper.make(Bean)).isEmpty()) { + continue } // A proxied host still cannot intercept a .staticMethod() bean: the interception is // CGLIB subclassing, and a static method cannot be overridden. So on a full // @Configuration class those are the only sibling calls still worth rejecting. if (!proxied || method.isStatic()) { - beanMethodsByName.put(method.getName(), method); + beanMethodsByName.put(method.getName(), method) } } if (beanMethodsByName.isEmpty()) { - return; + return } - for (MethodNode method : generated) { + for (MethodNode method in generated) { if (method.getCode() == null) { - continue; + continue } - MethodNode caller = method; + MethodNode caller = method method.getCode().visit(new CodeVisitorSupport() { // Deliberately not descending. Inside a closure an unqualified call is resolved // against the delegate first, so `new Registry().tap { initialize() }` calls the @@ -1649,25 +1634,25 @@ private void rejectUnproxiedSiblingBeanCalls(ClassNode host, List ge // Reading that as a sibling bean call would reject working code, which is a far // worse trade than missing the rare bean call written inside a nested closure. @Override - public void visitClosureExpression(ClosureExpression expression) { + void visitClosureExpression(ClosureExpression expression) { } @Override - public void visitMethodCallExpression(MethodCallExpression call) { - super.visitMethodCallExpression(call); + void visitMethodCallExpression(MethodCallExpression call) { + super.visitMethodCallExpression(call) if (!isSelfCall(call)) { - return; + return } - MethodNode target = beanMethodsByName.get(call.getMethodAsString()); + MethodNode target = beanMethodsByName.get(call.getMethodAsString()) if (target == null || target == caller) { - return; + return } addError(call, source, siblingCallCause(host, call.getMethodAsString(), proxied) + - ", so this call does not return the bean Spring registered - it constructs a second " + - "instance. Inject it instead, by declaring it as a parameter of this closure; if what " + - "you want is shared logic rather than the bean, move it into a method(...) declaration."); + ', so this call does not return the bean Spring registered - it constructs a second ' + + 'instance. Inject it instead, by declaring it as a parameter of this closure; if what ' + + 'you want is shared logic rather than the bean, move it into a method(...) declaration.') } - }); + }) } } @@ -1675,13 +1660,13 @@ public void visitMethodCallExpression(MethodCallExpression call) { // bean methods, so reaching here means the target is one. private String siblingCallCause(ClassNode host, String name, boolean proxied) { if (proxied) { - return "\"" + name + "(...)\" is another bean declared in this block, and is declared " + - ".staticMethod(). A static @Bean method is never intercepted by the container - not even " + - "on a proxied @Configuration class like " + host.getNameWithoutPackage() + " - because " + - "that interception is CGLIB subclassing, which cannot override a static method"; + return '"' + name + '(...)" is another bean declared in this block, and is declared ' + + '.staticMethod(). A static @Bean method is never intercepted by the container - not even ' + + 'on a proxied @Configuration class like ' + host.getNameWithoutPackage() + ' - because ' + + 'that interception is CGLIB subclassing, which cannot override a static method' } - return "\"" + name + "(...)\" is another bean declared in this block, and " + - host.getNameWithoutPackage() + " is not a proxied @Configuration class"; + return '"' + name + '(...)" is another bean declared in this block, and ' + + host.getNameWithoutPackage() + ' is not a proxied @Configuration class' } /** @@ -1714,31 +1699,31 @@ private String siblingCallCause(ClassNode host, String name, boolean proxied) { */ private void rejectAnonymousClassReachingOutward(ClassNode owner, boolean staticNestedOwner, List generatedMethods, SourceUnit source) { - for (MethodNode method : generatedMethods) { - List anonymous = new ArrayList<>(); + for (MethodNode method in generatedMethods) { + List anonymous = new ArrayList<>() CodeVisitorSupport collector = new CodeVisitorSupport() { @Override - public void visitConstructorCallExpression(ConstructorCallExpression call) { + void visitConstructorCallExpression(ConstructorCallExpression call) { if (call.isUsingAnonymousInnerClass()) { - anonymous.add(call); + anonymous.add(call) } - super.visitConstructorCallExpression(call); + super.visitConstructorCallExpression(call) } - }; + } // The same two roots the lift repairs - the body, and every parameter default that came // across with its parameter. A class the lift re-homed and this did not look at is the // one shape that reaches runtime with the mismatch and no diagnostic. if (method.getCode() != null) { - method.getCode().visit(collector); + method.getCode().visit(collector) } - for (Parameter parameter : method.getParameters()) { + for (Parameter parameter in method.getParameters()) { if (parameter.getInitialExpression() != null) { - parameter.getInitialExpression().visit(collector); + parameter.getInitialExpression().visit(collector) } } - for (ConstructorCallExpression call : anonymous) { + for (ConstructorCallExpression call in anonymous) { reportOutwardReferences(call.getType(), owner, staticNestedOwner, source, - new HashSet<>(), new HashSet<>(), EnclosingMethodMode.NONE); + new HashSet<>(), new HashSet<>(), EnclosingMethodMode.NONE) } } } @@ -1751,73 +1736,73 @@ private void reportOutwardReferences(ClassNode inner, ClassNode owner, boolean s SourceUnit source, Set enclosingReachable, Set visited, EnclosingMethodMode enclosingMode) { if (!visited.add(inner) || answersAnything(inner)) { - return; + return } - Set baseOwn = existingMemberNames(inner); - addExtensionMethodNames(inner, baseOwn); - baseOwn.addAll(enclosingReachable); + Set baseOwn = existingMemberNames(inner) + addExtensionMethodNames(inner, baseOwn) + baseOwn.addAll(enclosingReachable) // existingMemberNames walks the supertypes for METHOD names and for the accessors a property // reserves, but ClassNode.getFields() is declared fields only. Without the inherited ones, // `this.tag` would be reported where the bare `tag` is not - the variable path resolves it to // a real FieldNode and declaredWithin lets it through, so the two spellings must agree. for (ClassNode current = inner; current != null; current = current.getSuperClass()) { - for (FieldNode field : current.getFields()) { - baseOwn.add(field.getName()); + for (FieldNode field in current.getFields()) { + baseOwn.add(field.getName()) } } - Set enclosingStatics = enclosingStaticNames(inner); - Set ownWithStatics = new HashSet<>(baseOwn); - ownWithStatics.addAll(enclosingStatics); + Set enclosingStatics = enclosingStaticNames(inner) + Set ownWithStatics = new HashSet<>(baseOwn) + ownWithStatics.addAll(enclosingStatics) // An explicit SKIP propagates through every nested class. A method-level @CompileStatic // does something different: Groovy's GROOVY-9327 visit checks nested classes inline and // marks their methods as visited, so their own @CompileStatic cannot start another pass. // Those methods retain the class-chain answer unless they explicitly opt out. A plain // method propagates neither fact, so a nested method may still opt into static compilation. boolean classStaticsInReach = enclosingMode != EnclosingMethodMode.SKIPPED && - inner.getOuterClass() != null && isStaticallyCompiled(inner.getOuterClass()); - Set classOwn = classStaticsInReach ? ownWithStatics : baseOwn; + inner.getOuterClass() != null && isStaticallyCompiled(inner.getOuterClass()) + Set classOwn = classStaticsInReach ? ownWithStatics : baseOwn // Decide each body's reach separately from the annotation state passed to nested classes. - for (MethodNode method : inner.getMethods()) { + for (MethodNode method in inner.getMethods()) { if (method.getCode() == null) { - continue; + continue } - EnclosingMethodMode methodMode = methodMode(method); + EnclosingMethodMode methodMode = methodMode(method) boolean bodyStatic = switch (enclosingMode) { - case SKIPPED -> false; - case STATIC -> methodMode != EnclosingMethodMode.SKIPPED && classStaticsInReach; + case SKIPPED -> false + case STATIC -> methodMode != EnclosingMethodMode.SKIPPED && classStaticsInReach case NONE -> methodMode == EnclosingMethodMode.STATIC || - methodMode == EnclosingMethodMode.NONE && classStaticsInReach; - }; + methodMode == EnclosingMethodMode.NONE && classStaticsInReach + } // baseOwn, not the body's set, is what a class nested here inherits: the statics have to // reach it through its own answer or not at all, or a static body would hand them over // before that answer is ever consulted. recurseIntoNested(walkBody(method.getCode(), inner, owner, staticNestedOwner, source, enclosingStatics, bodyStatic ? ownWithStatics : baseOwn), - owner, staticNestedOwner, source, baseOwn, visited, nestedMethodMode(enclosingMode, methodMode)); + owner, staticNestedOwner, source, baseOwn, visited, nestedMethodMode(enclosingMode, methodMode)) } // A field initializer and an object initializer are the class's code too - the Verifier only // folds them into the constructor at class generation, long after this - and there is // nowhere to annotate them, so they keep the class's answer. - for (FieldNode field : inner.getFields()) { + for (FieldNode field in inner.getFields()) { if (field.getInitialExpression() != null) { recurseIntoNested(walkBody(field.getInitialExpression(), inner, owner, staticNestedOwner, source, enclosingStatics, classOwn), - owner, staticNestedOwner, source, baseOwn, visited, enclosingMode); + owner, staticNestedOwner, source, baseOwn, visited, enclosingMode) } } - for (Statement statement : inner.getObjectInitializerStatements()) { + for (Statement statement in inner.getObjectInitializerStatements()) { recurseIntoNested(walkBody(statement, inner, owner, staticNestedOwner, source, enclosingStatics, - classOwn), owner, staticNestedOwner, source, baseOwn, visited, enclosingMode); + classOwn), owner, staticNestedOwner, source, baseOwn, visited, enclosingMode) } } private void recurseIntoNested(List nested, ClassNode owner, boolean staticNestedOwner, SourceUnit source, Set enclosingReachable, Set visited, EnclosingMethodMode enclosingMode) { - for (ConstructorCallExpression call : nested) { + for (ConstructorCallExpression call in nested) { reportOutwardReferences(call.getType(), owner, staticNestedOwner, source, enclosingReachable, - visited, enclosingMode); + visited, enclosingMode) } } @@ -1828,112 +1813,112 @@ private enum EnclosingMethodMode { // @CompileDynamic arrives as @CompileStatic(TypeCheckingMode.SKIP). Absence is distinct from // SKIP: an unannotated method does not prevent a nested method from opting in on its own. private EnclosingMethodMode methodMode(MethodNode method) { - List annotations = method.getAnnotations(COMPILE_STATIC_TYPE); + List annotations = method.getAnnotations(COMPILE_STATIC_TYPE) if (annotations.isEmpty()) { - return EnclosingMethodMode.NONE; + return EnclosingMethodMode.NONE } return isTypeCheckingSkipped(annotations.get(0)) ? - EnclosingMethodMode.SKIPPED : EnclosingMethodMode.STATIC; + EnclosingMethodMode.SKIPPED : EnclosingMethodMode.STATIC } private EnclosingMethodMode nestedMethodMode(EnclosingMethodMode enclosing, EnclosingMethodMode method) { if (enclosing == EnclosingMethodMode.SKIPPED || method == EnclosingMethodMode.SKIPPED) { - return EnclosingMethodMode.SKIPPED; + return EnclosingMethodMode.SKIPPED } if (enclosing == EnclosingMethodMode.STATIC || method == EnclosingMethodMode.STATIC) { - return EnclosingMethodMode.STATIC; + return EnclosingMethodMode.STATIC } - return EnclosingMethodMode.NONE; + return EnclosingMethodMode.NONE } private List walkBody(ASTNode body, ClassNode inner, ClassNode owner, boolean staticNestedOwner, SourceUnit source, Set enclosingStatics, Set own) { - List nested = new ArrayList<>(); + List nested = new ArrayList<>() body.visit(new CodeVisitorSupport() { - // Deliberately not descending, the same reason rejectUnproxiedSiblingBeanCalls does - // not: a closure's resolve strategy and delegate are runtime facts, so an - // implicit-this call inside one may well be answered by a delegate rather than by - // this class - `sb.tap { append(x) }`, `s.with { toUpperCase() }`. Reporting there - // would reject working code, which is worse than the NoSuchFieldError it would have - // caught: an author who hits that one still gets an error naming the class. - @Override - public void visitClosureExpression(ClosureExpression expression) { - } + // Deliberately not descending, the same reason rejectUnproxiedSiblingBeanCalls does + // not: a closure's resolve strategy and delegate are runtime facts, so an + // implicit-this call inside one may well be answered by a delegate rather than by + // this class - `sb.tap { append(x) }`, `s.with { toUpperCase() }`. Reporting there + // would reject working code, which is worse than the NoSuchFieldError it would have + // caught: an author who hits that one still gets an error naming the class. + @Override + void visitClosureExpression(ClosureExpression expression) { + } - @Override - public void visitConstructorCallExpression(ConstructorCallExpression call) { - super.visitConstructorCallExpression(call); - if (call.isUsingAnonymousInnerClass()) { - nested.add(call); - } + @Override + void visitConstructorCallExpression(ConstructorCallExpression call) { + super.visitConstructorCallExpression(call) + if (call.isUsingAnonymousInnerClass()) { + nested.add(call) } + } - // Both spellings of a self-call. `this.suffix()` leaves the class exactly as - // `suffix()` does - isSelfCall is what the sibling-call check already uses to say so. - @Override - public void visitMethodCallExpression(MethodCallExpression inner) { - super.visitMethodCallExpression(inner); - if (isSelfCall(inner)) { - report(relatedNames(inner.getMethodAsString()), inner, "()"); - } + // Both spellings of a self-call. `this.suffix()` leaves the class exactly as + // `suffix()` does - isSelfCall is what the sibling-call check already uses to say so. + @Override + void visitMethodCallExpression(MethodCallExpression methodCall) { + super.visitMethodCallExpression(methodCall) + if (isSelfCall(methodCall)) { + report(relatedNames(methodCall.getMethodAsString()), methodCall, '()') } + } - // `this.suffix` is a PropertyExpression, not a variable, so it needs its own visit - - // and AttributeExpression (`this.@suffix`) routes through here too. - @Override - public void visitPropertyExpression(PropertyExpression expression) { - super.visitPropertyExpression(expression); - if (expression.isImplicitThis() || isThisExpression(expression.getObjectExpression())) { - report(relatedNames(expression.getPropertyAsString()), expression, ""); - } + // `this.suffix` is a PropertyExpression, not a variable, so it needs its own visit - + // and AttributeExpression (`this.@suffix`) routes through here too. + @Override + void visitPropertyExpression(PropertyExpression expression) { + super.visitPropertyExpression(expression) + if (expression.isImplicitThis() || isThisExpression(expression.getObjectExpression())) { + report(relatedNames(expression.getPropertyAsString()), expression, '') } + } - @Override - public void visitVariableExpression(VariableExpression expression) { - super.visitVariableExpression(expression); - Variable accessed = expression.getAccessedVariable(); - // A dynamic variable resolved to nothing, so it can only be answered through - // this$0. A field or property resolved to a real declaration is the subtler - // case: VariableScopeVisitor searches the ENCLOSING class too, so an outer - // member reads as resolved while still needing this$0 to be fetched. A local or - // a parameter is neither - the lift copies those into the class, which is why - // a captured local is the way out this error recommends. - if (accessed instanceof DynamicVariable || - !declaredWithin(inner, declaringClassOf(accessed))) { - report(relatedNames(expression.getName()), expression, ""); - } + @Override + void visitVariableExpression(VariableExpression expression) { + super.visitVariableExpression(expression) + Variable accessed = expression.getAccessedVariable() + // A dynamic variable resolved to nothing, so it can only be answered through + // this$0. A field or property resolved to a real declaration is the subtler + // case: VariableScopeVisitor searches the ENCLOSING class too, so an outer + // member reads as resolved while still needing this$0 to be fetched. A local or + // a parameter is neither - the lift copies those into the class, which is why + // a captured local is the way out this error recommends. + if (accessed instanceof DynamicVariable || + !declaredWithin(inner, declaringClassOf(accessed))) { + report(relatedNames(expression.getName()), expression, '') } + } - private void report(List spellings, ASTNode at, String callSuffix) { - String name = spellings.get(0); - if (name == null || !Collections.disjoint(spellings, own)) { - return; - } - addError(at, source, "\"" + name + callSuffix + "\" does not resolve on this anonymous " + - "inner class, and " + reachDescription(owner, staticNestedOwner) + ". The reference would " + - "fail at runtime - with NoSuchFieldError, or a ClassCastException where the class " + - "sits inside a nested closure. Pass what the anonymous class needs as a constructor " + - "argument or a captured local, or give it a name and declare it as a static nested " + - "class." + (Collections.disjoint(spellings, enclosingStatics) ? "" : - " This one is a static member of an enclosing class, which a statically " + - "compiled host would reach by invokestatic - here it goes through the " + - "enclosing instance, so qualifying it with the declaring class name is " + - "enough.")); + private void report(List spellings, ASTNode at, String callSuffix) { + String name = spellings.get(0) + if (name == null || !Collections.disjoint(spellings, own)) { + return } - }); - return nested; + addError(at, source, '"' + name + callSuffix + '" does not resolve on this anonymous ' + + 'inner class, and ' + reachDescription(owner, staticNestedOwner) + '. The reference would ' + + 'fail at runtime - with NoSuchFieldError, or a ClassCastException where the class ' + + 'sits inside a nested closure. Pass what the anonymous class needs as a constructor ' + + 'argument or a captured local, or give it a name and declare it as a static nested ' + + 'class.' + (Collections.disjoint(spellings, enclosingStatics) ? '' : + ' This one is a static member of an enclosing class, which a statically ' + + 'compiled host would reach by invokestatic - here it goes through the ' + + 'enclosing instance, so qualifying it with the declaring class name is ' + + 'enough.')) + } + }) + return nested } private String reachDescription(ClassNode owner, boolean staticNestedOwner) { if (staticNestedOwner) { - return "these beans compile onto " + owner.getNameWithoutPackage() + ", a static nested " + - "class with no enclosing instance behind it, so nothing outside the anonymous class " + - "is in reach"; + return 'these beans compile onto ' + owner.getNameWithoutPackage() + ', a static nested ' + + 'class with no enclosing instance behind it, so nothing outside the anonymous class ' + + 'is in reach' } return "this block's beans compile onto the generated sibling " + owner.getNameWithoutPackage() + - " - while the anonymous class keeps the plugin descriptor as its outer class, which Groovy " + - "fixes when it creates the class and this cannot move, so nothing outside the anonymous " + - "class is in reach"; + ' - while the anonymous class keeps the plugin descriptor as its outer class, which Groovy ' + + 'fixes when it creates the class and this cannot move, so nothing outside the anonymous ' + + 'class is in reach' } // Groovy's extension methods, by name, each against the receiver types it is declared for. An @@ -1948,111 +1933,111 @@ private String reachDescription(ClassNode owner, boolean staticNestedOwner) { // its accessor names. Extension modules contributed by the classpath (groovy-nio's // NioExtensions, groovy-datetime) are not in DGM_LIKE_CLASSES; an anonymous class whose // receiver is one of those would be reported and is unusual enough to leave. - private static final Map> EXTENSION_RECEIVERS = extensionReceivers(); + private static final Map> EXTENSION_RECEIVERS = extensionReceivers() private static Map> extensionReceivers() { - Map> receivers = new HashMap<>(); - for (Class category : DefaultGroovyMethods.DGM_LIKE_CLASSES) { - for (Method method : category.getMethods()) { + Map> receivers = new HashMap<>() + for (Class category in DefaultGroovyMethods.DGM_LIKE_CLASSES) { + for (Method method in category.getMethods()) { if (Modifier.isStatic(method.getModifiers()) && method.getParameterCount() > 0) { receivers.computeIfAbsent(method.getName(), name -> new HashSet<>()) - .add(method.getParameterTypes()[0].getName()); + .add(method.getParameterTypes()[0].getName()) } } } - return receivers; + return receivers } private void addExtensionMethodNames(ClassNode inner, Set own) { - Set receiverTypes = new HashSet<>(); - receiverTypes.add(ClassHelper.OBJECT_TYPE.getName()); + Set receiverTypes = new HashSet<>() + receiverTypes.add(ClassHelper.OBJECT_TYPE.getName()) // getAllInterfaces() is transitive over a class's OWN interfaces and does not climb the // superclass chain, so it has to be asked of each superclass in turn - otherwise an // anonymous class extending an ArrayList subclass shows no Iterable and loses `join`. for (ClassNode current = inner; current != null; current = current.getSuperClass()) { - receiverTypes.add(current.getName()); - for (ClassNode implemented : current.getAllInterfaces()) { - receiverTypes.add(implemented.getName()); + receiverTypes.add(current.getName()) + for (ClassNode implemented in current.getAllInterfaces()) { + receiverTypes.add(implemented.getName()) } } - for (Map.Entry> extension : EXTENSION_RECEIVERS.entrySet()) { + for (Map.Entry> extension in EXTENSION_RECEIVERS.entrySet()) { if (!Collections.disjoint(extension.getValue(), receiverTypes)) { - own.add(extension.getKey()); + own.add(extension.getKey()) } } } - private static final ClassNode COMPILE_STATIC_TYPE = ClassHelper.make(CompileStatic.class); + private static final ClassNode COMPILE_STATIC_TYPE = ClassHelper.make(CompileStatic) // A static member of an enclosing class is reached by invokestatic under @CompileStatic and // never through this$0 - measured with javap - so it is in reach there. On a dynamic host the // same reference goes through getProperty/invokeMethod on this$0 and is not, which is why this // is conditional rather than unconditional. private Set enclosingStaticNames(ClassNode inner) { - Set names = new HashSet<>(); + Set names = new HashSet<>() for (ClassNode outer = inner.getOuterClass(); outer != null; outer = outer.getOuterClass()) { for (ClassNode current = outer; current != null; current = current.getSuperClass()) { - for (MethodNode method : current.getMethods()) { + for (MethodNode method in current.getMethods()) { if (method.isStatic()) { - names.add(method.getName()); + names.add(method.getName()) } } - for (FieldNode field : current.getFields()) { + for (FieldNode field in current.getFields()) { if (field.isStatic()) { - names.add(field.getName()); + names.add(field.getName()) } } } } - return names; + return names } // @GrailsCompileStatic and @GrailsTypeChecked need no handling of their own here either: // @AnnotationCollector has already expanded them by canonicalization. private boolean isStaticallyCompiled(ClassNode type) { for (ClassNode current = type; current != null; current = current.getOuterClass()) { - List annotations = current.getAnnotations(COMPILE_STATIC_TYPE); + List annotations = current.getAnnotations(COMPILE_STATIC_TYPE) if (!annotations.isEmpty()) { // The innermost one decides, so stop here either way. @CompileDynamic is an // @AnnotationCollector for @CompileStatic(TypeCheckingMode.SKIP) and is expanded // before canonicalization, so it arrives looking identical to the real thing while // its bytecode is dynamic - which is the only thing being asked about. - return !isTypeCheckingSkipped(annotations.get(0)); + return !isTypeCheckingSkipped(annotations.get(0)) } } - return false; + return false } private boolean isTypeCheckingSkipped(AnnotationNode annotation) { - Expression mode = annotation.getMember("value"); + Expression mode = annotation.getMember('value') if (mode instanceof PropertyExpression) { - return TypeCheckingMode.SKIP.name().equals(((PropertyExpression) mode).getPropertyAsString()); + return TypeCheckingMode.SKIP.name().equals(((PropertyExpression) mode).getPropertyAsString()) } if (mode instanceof ConstantExpression) { // endsWith rather than equals on purpose: a collector that folded the member to a // constant yields the bare enum name, but a qualified spelling would still be this // mode, and no other TypeCheckingMode constant ends this way. - return String.valueOf(((ConstantExpression) mode).getValue()).endsWith(TypeCheckingMode.SKIP.name()); + return String.valueOf(((ConstantExpression) mode).getValue()).endsWith(TypeCheckingMode.SKIP.name()) } - return false; + return false } // Null for anything that is not a member declaration - a local, a parameter, a dynamic variable - // which declaredWithin then treats as in reach, because it is. private static ClassNode declaringClassOf(Variable accessed) { if (accessed instanceof FieldNode) { - return ((FieldNode) accessed).getDeclaringClass(); + return ((FieldNode) accessed).getDeclaringClass() } if (accessed instanceof PropertyNode) { - FieldNode field = ((PropertyNode) accessed).getField(); - return field != null ? field.getDeclaringClass() : null; + FieldNode field = ((PropertyNode) accessed).getField() + return field != null ? field.getDeclaringClass() : null } - return null; + return null } private boolean declaredWithin(ClassNode inner, ClassNode declaring) { return declaring == null || inner.getName().equals(declaring.getName()) || - inner.isDerivedFrom(declaring) || inner.implementsInterface(declaring); + inner.isDerivedFrom(declaring) || inner.implementsInterface(declaring) } // A class carrying its own methodMissing/propertyMissing/invokeMethod/getProperty answers any @@ -2060,9 +2045,9 @@ private boolean declaredWithin(ClassNode inner, ClassNode declaring) { private boolean answersAnything(ClassNode inner) { for (ClassNode current = inner; current != null && !ClassHelper.OBJECT_TYPE.getName().equals(current.getName()); current = current.getSuperClass()) { - if (!current.getDeclaredMethods("methodMissing").isEmpty() || - !current.getDeclaredMethods("propertyMissing").isEmpty()) { - return true; + if (!current.getDeclaredMethods('methodMissing').isEmpty() || + !current.getDeclaredMethods('propertyMissing').isEmpty()) { + return true } // invokeMethod and getProperty only say that when somebody wrote them. They became // GroovyObject default methods in Groovy 3; a superclass from a library compiled before @@ -2070,12 +2055,12 @@ private boolean answersAnything(ClassNode inner) { // check entirely. getModule() is non-null only for a class this unit compiles from // source, which is where "somebody wrote them" can actually be told apart. if (current.getModule() != null && - (!current.getDeclaredMethods("invokeMethod").isEmpty() || - !current.getDeclaredMethods("getProperty").isEmpty())) { - return true; + (!current.getDeclaredMethods('invokeMethod').isEmpty() || + !current.getDeclaredMethods('getProperty').isEmpty())) { + return true } } - return false; + return false } // Every name one reference could resolve to, itself included, in both directions: `suffix` also @@ -2085,35 +2070,35 @@ private boolean answersAnything(ClassNode inner) { // reserved under in collectMethodNames. private static List relatedNames(String name) { if (name == null) { - return Collections.singletonList(null); - } - List names = new ArrayList<>(); - names.add(name); - String capitalized = BeanUtils.capitalize(name); - names.add("get" + capitalized); - names.add("is" + capitalized); - names.add("set" + capitalized); - String property = propertyNameOf(name); + return Collections.singletonList(null) + } + List names = new ArrayList<>() + names.add(name) + String capitalized = BeanUtils.capitalize(name) + names.add('get' + capitalized) + names.add('is' + capitalized) + names.add('set' + capitalized) + String property = propertyNameOf(name) if (property != null) { - names.add(property); + names.add(property) } - return names; + return names } // "getSuffix" -> "suffix", "isReady" -> "ready", "setSuffix" -> "suffix"; anything that is not an // accessor name -> null. The uppercase test is what keeps "getaway" and "issue" out of it. private static String propertyNameOf(String name) { - for (String prefix : new String[] { "get", "set", "is" }) { + for (String prefix in ['get', 'set', 'is'] as String[]) { if (name.length() > prefix.length() && name.startsWith(prefix) && Character.isUpperCase(name.charAt(prefix.length()))) { - return Introspector.decapitalize(name.substring(prefix.length())); + return Introspector.decapitalize(name.substring(prefix.length())) } } - return null; + return null } private boolean isThisExpression(Expression expression) { - return expression instanceof VariableExpression && ((VariableExpression) expression).isThisExpression(); + return expression instanceof VariableExpression && ((VariableExpression) expression).isThisExpression() } // An unqualified call, or one written against this. Anything with a real receiver is somebody @@ -2121,7 +2106,7 @@ private boolean isThisExpression(Expression expression) { private boolean isSelfCall(MethodCallExpression call) { return call.isImplicitThis() || (call.getObjectExpression() instanceof VariableExpression && - ((VariableExpression) call.getObjectExpression()).isThisExpression()); + ((VariableExpression) call.getObjectExpression()).isThisExpression()) } // Whether Spring will CGLIB-proxy this host's @Bean methods: true only when @Configuration is @@ -2130,7 +2115,7 @@ private boolean isSelfCall(MethodCallExpression call) { // meta-annotation is @Configuration(proxyBeanMethods = false) - and a Grails Application class // answers false by carrying no @Configuration at all. private boolean beanMethodsAreProxied(ClassNode host) { - return proxiesBeanMethods(host.getAnnotations(), new HashSet<>()); + return proxiesBeanMethods(host.getAnnotations(), new HashSet<>()) } // `visited` guards descent, and only descent. Recording a type when the branch is pruned - by @@ -2139,32 +2124,32 @@ private boolean beanMethodsAreProxied(ClassNode host) { // @AutoConfiguration is pruned, and a real proxying @Configuration written alongside it would // then be skipped as already-seen, answering false for a class Spring does proxy. private boolean proxiesBeanMethods(List annotations, Set visited) { - for (AnnotationNode annotation : annotations) { - ClassNode type = annotation.getClassNode(); - if (type.getName().startsWith("java.lang.annotation.")) { - continue; + for (AnnotationNode annotation in annotations) { + ClassNode type = annotation.getClassNode() + if (type.getName().startsWith('java.lang.annotation.')) { + continue } if (isFalseConstant(annotation.getMember(PROXY_BEAN_METHODS_MEMBER))) { - continue; + continue } - if (Configuration.class.getName().equals(type.getName())) { - return true; + if (Configuration.getName().equals(type.getName())) { + return true } if (visited.add(type.getName()) && proxiesBeanMethods(type.getAnnotations(), visited)) { - return true; + return true } } - return false; + return false } private boolean isFalseConstant(Expression expression) { return expression instanceof ConstantExpression && - Boolean.FALSE.equals(((ConstantExpression) expression).getValue()); + Boolean.FALSE.equals(((ConstantExpression) expression).getValue()) } private boolean isEmpty(Statement code) { return code == null || code instanceof EmptyStatement || - (code instanceof BlockStatement && ((BlockStatement) code).getStatements().isEmpty()); + (code instanceof BlockStatement && ((BlockStatement) code).getStatements().isEmpty()) } /** @@ -2181,20 +2166,20 @@ private boolean isEmpty(Statement code) { * constructor" the hand-written equivalent gets.

*/ private Statement synthesizedConstruction(ClassNode beanType, Parameter[] parameters, ASTNode origin) { - ArgumentListExpression arguments = new ArgumentListExpression(); - for (Parameter parameter : parameters) { - VariableExpression argument = new VariableExpression(parameter); - argument.setSourcePosition(origin); - arguments.addExpression(argument); + ArgumentListExpression arguments = new ArgumentListExpression() + for (Parameter parameter in parameters) { + VariableExpression argument = new VariableExpression(parameter) + argument.setSourcePosition(origin) + arguments.addExpression(argument) } - arguments.setSourcePosition(origin); + arguments.setSourcePosition(origin) - ConstructorCallExpression construction = new ConstructorCallExpression(beanType, arguments); - construction.setSourcePosition(origin); + ConstructorCallExpression construction = new ConstructorCallExpression(beanType, arguments) + construction.setSourcePosition(origin) - ReturnStatement returnStatement = new ReturnStatement(construction); - returnStatement.setSourcePosition(origin); - return returnStatement; + ReturnStatement returnStatement = new ReturnStatement(construction) + returnStatement.setSourcePosition(origin) + return returnStatement } /** @@ -2204,18 +2189,18 @@ private Statement synthesizedConstruction(ClassNode beanType, Parameter[] parame * an assignment error inside a body the author never wrote. */ private boolean isSubtypeOf(ClassNode candidate, ClassNode target) { - ClassNode resolved = target.redirect(); + ClassNode resolved = target.redirect() return candidate.redirect().equals(resolved) || candidate.isDerivedFrom(resolved) || - candidate.implementsInterface(resolved); + candidate.implementsInterface(resolved) } private boolean hasExplicitTypeArguments(List qualifierCalls) { - for (MethodCallExpression qualifierCall : qualifierCalls) { + for (MethodCallExpression qualifierCall in qualifierCalls) { if (TYPE_ARGUMENTS_CALL.equals(qualifierCall.getMethodAsString())) { - return true; + return true } } - return false; + return false } /** @@ -2226,21 +2211,21 @@ private boolean hasExplicitTypeArguments(List qualifierCal */ private ClassNode constructedTypeFromBody(ClosureExpression factory) { if (factory == null || !(factory.getCode() instanceof BlockStatement)) { - return null; + return null } - List statements = ((BlockStatement) factory.getCode()).getStatements(); + List statements = ((BlockStatement) factory.getCode()).getStatements() if (statements.isEmpty()) { - return null; + return null } - Statement last = statements.get(statements.size() - 1); - Expression expression = null; + Statement last = statements.get(statements.size() - 1) + Expression expression = null if (last instanceof ReturnStatement) { - expression = ((ReturnStatement) last).getExpression(); + expression = ((ReturnStatement) last).getExpression() } else if (last instanceof ExpressionStatement) { - expression = ((ExpressionStatement) last).getExpression(); + expression = ((ExpressionStatement) last).getExpression() } - return expression instanceof ConstructorCallExpression ? expression.getType() : null; + return expression instanceof ConstructorCallExpression ? expression.getType() : null } /** @@ -2252,42 +2237,42 @@ else if (last instanceof ExpressionStatement) { * {@code .typeArguments(...)} remains the way to say it. */ private ClassNode inferTypeArguments(ClassNode declaredRaw, ClassNode evidence) { - GenericsType[] declared = declaredRaw.redirect().getGenericsTypes(); + GenericsType[] declared = declaredRaw.redirect().getGenericsTypes() if (evidence == null || declared == null || declared.length == 0) { - return null; + return null } if (!isSubtypeOf(evidence, declaredRaw)) { - return null; + return null } // A raw construction of a generic type proves nothing: Groovy resolves its parameters to // their bounds, so new GenericBox() would infer Holder - not merely uninformative // but wrong, since a bean typed Holder no longer matches a Holder injection // point it previously did as a raw Holder. - GenericsType[] evidenceParameters = evidence.redirect().getGenericsTypes(); + GenericsType[] evidenceParameters = evidence.redirect().getGenericsTypes() if (evidenceParameters != null && evidenceParameters.length > 0 && (evidence.getGenericsTypes() == null || evidence.getGenericsTypes().length == 0)) { - return null; + return null } - ClassNode parameterized; + ClassNode parameterized try { - parameterized = GenericsUtils.parameterizeType(evidence, declaredRaw.redirect()); + parameterized = GenericsUtils.parameterizeType(evidence, declaredRaw.redirect()) } catch (RuntimeException ignored) { // parameterizeType is best-effort on partially resolved hierarchies; an unusable answer // is the same as no answer. - return null; + return null } - GenericsType[] resolved = parameterized == null ? null : parameterized.getGenericsTypes(); + GenericsType[] resolved = parameterized == null ? null : parameterized.getGenericsTypes() if (resolved == null || resolved.length != declared.length) { - return null; + return null } - for (GenericsType candidate : resolved) { + for (GenericsType candidate in resolved) { if (candidate.isPlaceholder() || candidate.isWildcard() || candidate.getType() == null || candidate.getType().isGenericsPlaceHolder()) { - return null; + return null } } - return GenericsUtils.makeClassSafeWithGenerics(declaredRaw, resolved); + return GenericsUtils.makeClassSafeWithGenerics(declaredRaw, resolved) } /** @@ -2319,29 +2304,29 @@ private boolean rehomeAnonymousInnerClasses(MethodNode liftedMethod, ClassNode h String beanName, SourceUnit source) { // The body is not the only code the lift moved: a closure parameter's default value comes // across with the parameter, so an anonymous class written there needs the same repairs. - List roots = new ArrayList<>(); - roots.add(liftedMethod.getCode()); - for (Parameter parameter : liftedMethod.getParameters()) { + List roots = new ArrayList<>() + roots.add(liftedMethod.getCode()) + for (Parameter parameter in liftedMethod.getParameters()) { if (parameter.getInitialExpression() != null) { - roots.add(parameter.getInitialExpression()); + roots.add(parameter.getInitialExpression()) } } // Every anonymous class in there needs its enclosing method, one inside a nested closure // included: the parser sets it per enclosing METHOD, so a closure in between makes no // difference to what it would have written. This walk descends for that reason; the // re-homing below deliberately does not, re-homing being the narrower repair. - for (ASTNode root : roots) { + for (ASTNode root in roots) { root.visit(new CodeVisitorSupport() { @Override - public void visitConstructorCallExpression(ConstructorCallExpression call) { - super.visitConstructorCallExpression(call); + void visitConstructorCallExpression(ConstructorCallExpression call) { + super.visitConstructorCallExpression(call) if (call.isUsingAnonymousInnerClass() && call.getType().getEnclosingMethod() == null) { - call.getType().setEnclosingMethod(liftedMethod); + call.getType().setEnclosingMethod(liftedMethod) } } - }); + }) } - List anonymous = new ArrayList<>(); + List anonymous = new ArrayList<>() CodeVisitorSupport collector = new CodeVisitorSupport() { // Deliberately not descending. The lift moves the closure's own body into a method, so // an anonymous class written directly in it loses the closure it was homed against - @@ -2351,105 +2336,105 @@ public void visitConstructorCallExpression(ConstructorCallExpression call) { // GroovyCastException this method exists to prevent - or, under .staticMethod(), reject // a body that has an enclosing instance and compiles perfectly well. @Override - public void visitClosureExpression(ClosureExpression expression) { + void visitClosureExpression(ClosureExpression expression) { } @Override - public void visitConstructorCallExpression(ConstructorCallExpression call) { + void visitConstructorCallExpression(ConstructorCallExpression call) { if (call.isUsingAnonymousInnerClass()) { - anonymous.add(call); + anonymous.add(call) } - super.visitConstructorCallExpression(call); + super.visitConstructorCallExpression(call) } - }; - for (ASTNode root : roots) { - root.visit(collector); } - for (ConstructorCallExpression call : anonymous) { - ClassNode inner = call.getType(); - FieldNode outerField = inner.getDeclaredField("this$0"); + for (ASTNode root in roots) { + root.visit(collector) + } + for (ConstructorCallExpression call in anonymous) { + ClassNode inner = call.getType() + FieldNode outerField = inner.getDeclaredField('this\$0') if (outerField == null || !ClassHelper.CLOSURE_TYPE.equals(outerField.getType())) { - continue; // already homed somewhere real, or static - nothing the lift broke + continue // already homed somewhere real, or static - nothing the lift broke } // A static factory method has no enclosing instance to give it, and the field cannot be // dropped here: InnerClassVisitor added it and the constructor body assigns it. if (staticMethod) { - addError(call, source, "\"" + beanName + "\" is declared .staticMethod() and its body " + - "constructs an anonymous inner class, which needs an enclosing instance the " + - "static method has not got - give the anonymous class a name and declare it " + - "as a static nested class, or drop .staticMethod()"); - return false; - } - ClassNode enclosing = host.getPlainNodeReference(); - outerField.setType(enclosing); - for (ConstructorNode constructor : inner.getDeclaredConstructors()) { - Parameter[] parameters = constructor.getParameters(); + addError(call, source, '"' + beanName + '" is declared .staticMethod() and its body ' + + 'constructs an anonymous inner class, which needs an enclosing instance the ' + + 'static method has not got - give the anonymous class a name and declare it ' + + 'as a static nested class, or drop .staticMethod()') + return false + } + ClassNode enclosing = host.getPlainNodeReference() + outerField.setType(enclosing) + for (ConstructorNode constructor in inner.getDeclaredConstructors()) { + Parameter[] parameters = constructor.getParameters() if (parameters.length > 0 && ClassHelper.CLOSURE_TYPE.equals(parameters[0].getType())) { - parameters[0].setType(enclosing); + parameters[0].setType(enclosing) // Both, and originType is the one that matters: static type checking compares // arguments against Parameter.getOriginType() while the error it raises prints // getType(), so setting only the latter fails the call and reports the two // types as identical. - parameters[0].setOriginType(enclosing); + parameters[0].setOriginType(enclosing) } } - List arguments = ((TupleExpression) call.getArguments()).getExpressions(); + List arguments = ((TupleExpression) call.getArguments()).getExpressions() if (!arguments.isEmpty() && arguments.get(0) instanceof VariableExpression && - "this".equals(((VariableExpression) arguments.get(0)).getName())) { - VariableExpression thisExpression = new VariableExpression("this", enclosing); - thisExpression.setSourcePosition(arguments.get(0)); - arguments.set(0, thisExpression); + 'this'.equals(((VariableExpression) arguments.get(0)).getName())) { + VariableExpression thisExpression = new VariableExpression('this', enclosing) + thisExpression.setSourcePosition(arguments.get(0)) + arguments.set(0, thisExpression) } } - return true; + return true } private String syntheticBeanMethodName(ClassNode beanType, Set usedNames) { - String base = decapitalize(simpleName(beanType)); - String candidate; - int index = 0; + String base = decapitalize(simpleName(beanType)) + String candidate + int index = 0 do { - candidate = base + "$" + index; - index++; + candidate = base + '\$' + index + index++ } - while (usedNames.contains(candidate)); - return candidate; + while (usedNames.contains(candidate)) + return candidate } private void processFieldStatement(ClassNode classNode, ClassNode declaringClass, MethodCallExpression baseCall, List qualifierCalls, SourceUnit source, Set usedNames) { - List baseArgs = flatten(baseCall.getArguments()); - TypeAndName typeAndName = parseNameAndType(baseArgs, baseCall, source, FIELD_CALL, true, declaringClass); + List baseArgs = flatten(baseCall.getArguments()) + TypeAndName typeAndName = parseNameAndType(baseArgs, baseCall, source, FIELD_CALL, true, declaringClass) if (typeAndName == null) { - return; + return } if (!registerName(typeAndName.name, baseCall, source, usedNames, - "is already used by another member of the class (declared, inherited, or another field(...)/method(...) statement) - " + - "generated member names must be unique")) { - return; + 'is already used by another member of the class (declared, inherited, or another field(...)/method(...) statement) - ' + + 'generated member names must be unique')) { + return } - ClassNode fieldType = declaredType(typeAndName, qualifierCalls, null, false, source, FIELD_CALL); + ClassNode fieldType = declaredType(typeAndName, qualifierCalls, null, false, source, FIELD_CALL) if (fieldType == null) { - return; + return } - FieldNode field = classNode.addField(typeAndName.name, Modifier.PRIVATE, fieldType, null); - field.setSourcePosition(baseCall); + FieldNode field = classNode.addField(typeAndName.name, Modifier.PRIVATE, fieldType, null) + field.setSourcePosition(baseCall) - for (MethodCallExpression qualifierCall : qualifierCalls) { + for (MethodCallExpression qualifierCall in qualifierCalls) { if (TYPE_ARGUMENTS_CALL.equals(qualifierCall.getMethodAsString())) { - continue; + continue } - List qualifierArgs = flatten(qualifierCall.getArguments()); + List qualifierArgs = flatten(qualifierCall.getArguments()) if (VALUE_CALL.equals(qualifierCall.getMethodAsString())) { - AnnotationNode valueAnnotation = valueAnnotation(qualifierArgs, qualifierCall, source); + AnnotationNode valueAnnotation = valueAnnotation(qualifierArgs, qualifierCall, source) if (valueAnnotation == null || !addAnnotationIfAbsent(field, qualifierCall, valueAnnotation, source)) { - return; + return } } else if (!applyGenericAnnotation(field, qualifierCall, qualifierArgs, source)) { - return; + return } } } @@ -2464,51 +2449,51 @@ else if (!applyGenericAnnotation(field, qualifierCall, qualifierArgs, source)) { // .annotate(Value, value: ...). private AnnotationNode valueAnnotation(List args, MethodCallExpression qualifierCall, SourceUnit source) { if (args.isEmpty() || args.size() > 2) { - addError(qualifierCall, source, ".value(...) requires a config key and default - e.g. " + - ".value(Settings.GSP_VIEW_ENCODING, \"UTF-8\") - or a single config key/placeholder/SpEL string"); - return null; + addError(qualifierCall, source, '.value(...) requires a config key and default - e.g. ' + + '.value(Settings.GSP_VIEW_ENCODING, "UTF-8") - or a single config key/placeholder/SpEL string') + return null } // The pieces are resolved and folded HERE, into a plain constant, rather than being left // as a concatenation for Groovy's own annotation folding: under @CompileStatic the static // compiler rewrites '+' into .plus() calls before that folding runs, which would reject // the member as a non-constant. - String memberValue; + String memberValue if (args.size() == 1) { - String placeholder = resolveStringConstant(args.get(0)); + String placeholder = resolveStringConstant(args.get(0)) if (placeholder == null) { - addError(args.get(0), source, ".value(...) arguments must be compile-time String constants " + - "(a literal, a static final constant reference, or a concatenation of those)"); - return null; + addError(args.get(0), source, '.value(...) arguments must be compile-time String constants ' + + '(a literal, a static final constant reference, or a concatenation of those)') + return null } if (placeholder.isBlank()) { - addError(args.get(0), source, ".value(...) requires a non-blank config key - a blank one " + - "would compile to the unresolvable placeholder ${}"); - return null; + addError(args.get(0), source, '.value(...) requires a non-blank config key - a blank one ' + + 'would compile to the unresolvable placeholder \${}') + return null } - memberValue = placeholder.contains("${") || placeholder.contains("#{") ? - placeholder : "${" + placeholder + "}"; + memberValue = placeholder.contains('\${') || placeholder.contains('#{') ? + placeholder : '\${' + placeholder + '}' } else { - String key = resolveStringConstant(args.get(0)); - String defaultValue = resolveStringConstant(args.get(1)); + String key = resolveStringConstant(args.get(0)) + String defaultValue = resolveStringConstant(args.get(1)) if (key == null || defaultValue == null) { - addError(key == null ? args.get(0) : args.get(1), source, ".value(...) arguments must be " + - "compile-time String constants (a literal, a static final constant reference, or a " + - "concatenation of those)"); - return null; + addError(key == null ? args.get(0) : args.get(1), source, '.value(...) arguments must be ' + + 'compile-time String constants (a literal, a static final constant reference, or a ' + + 'concatenation of those)') + return null } // Only the KEY must be non-blank: a deliberately blank default ('${key:}') is legal // and used (e.g. grails.i18n.default.locale falls back to the JVM default locale). if (key.isBlank()) { - addError(args.get(0), source, ".value(key, default) requires a non-blank config key - " + - "only the default may be blank"); - return null; + addError(args.get(0), source, '.value(key, default) requires a non-blank config key - ' + + 'only the default may be blank') + return null } - memberValue = "${" + key + ":" + defaultValue + "}"; + memberValue = '\${' + key + ':' + defaultValue + '}' } - AnnotationNode annotation = new AnnotationNode(ClassHelper.make(Value.class)); - annotation.setMember("value", new ConstantExpression(memberValue)); - return annotation; + AnnotationNode annotation = new AnnotationNode(ClassHelper.make(Value)) + annotation.setMember('value', new ConstantExpression(memberValue)) + return annotation } // Resolves an expression to its compile-time String value: literals directly; a static final @@ -2524,60 +2509,60 @@ private AnnotationNode valueAnnotation(List args, MethodCallExpressi * is the spelling that matters here; the qualified form already resolved.

*/ private String resolveStringConstant(Expression expression) { - return resolveStringConstant(expression, null); + return resolveStringConstant(expression, null) } private String resolveStringConstant(Expression expression, ClassNode declaringClass) { if (declaringClass != null && expression instanceof VariableExpression) { FieldNode field = findStaticFinalField(declaringClass, ((VariableExpression) expression).getName(), - new HashSet<>()); + new HashSet<>()) if (field != null && field.getInitialExpression() instanceof ConstantExpression) { - Object value = ((ConstantExpression) field.getInitialExpression()).getValue(); - return value instanceof String ? (String) value : null; + Object value = ((ConstantExpression) field.getInitialExpression()).getValue() + return value instanceof String ? (String) value : null } - return null; + return null } if (expression instanceof ConstantExpression) { - Object value = ((ConstantExpression) expression).getValue(); - return value instanceof String ? (String) value : null; + Object value = ((ConstantExpression) expression).getValue() + return value instanceof String ? (String) value : null } if (expression instanceof BinaryExpression) { - BinaryExpression binary = (BinaryExpression) expression; + BinaryExpression binary = (BinaryExpression) expression if (binary.getOperation().getType() != Types.PLUS) { - return null; + return null } - String left = resolveStringConstant(binary.getLeftExpression(), declaringClass); - String right = resolveStringConstant(binary.getRightExpression(), declaringClass); - return left != null && right != null ? left + right : null; + String left = resolveStringConstant(binary.getLeftExpression(), declaringClass) + String right = resolveStringConstant(binary.getRightExpression(), declaringClass) + return left != null && right != null ? left + right : null } if (expression instanceof PropertyExpression) { - PropertyExpression property = (PropertyExpression) expression; + PropertyExpression property = (PropertyExpression) expression if (!(property.getObjectExpression() instanceof ClassExpression)) { - return null; + return null } - ClassNode owner = property.getObjectExpression().getType(); - String fieldName = property.getPropertyAsString(); + ClassNode owner = property.getObjectExpression().getType() + String fieldName = property.getPropertyAsString() if (fieldName == null) { - return null; + return null } - FieldNode field = findStaticFinalField(owner, fieldName, new HashSet<>()); + FieldNode field = findStaticFinalField(owner, fieldName, new HashSet<>()) if (field != null && field.getInitialExpression() instanceof ConstantExpression) { - Object value = ((ConstantExpression) field.getInitialExpression()).getValue(); - return value instanceof String ? (String) value : null; + Object value = ((ConstantExpression) field.getInitialExpression()).getValue() + return value instanceof String ? (String) value : null } // The reflective fallback needs a loaded Class, which a ClassNode from this same // compilation unit does not have - getTypeClass() would throw GroovyBugError, an // AssertionError that no catch below would hold, aborting the compilation with an // internal compiler error instead of the located message the caller reports. try { - Object value = owner.getTypeClass().getField(fieldName).get(null); - return value instanceof String ? (String) value : null; + Object value = owner.getTypeClass().getField(fieldName).get(null) + return value instanceof String ? (String) value : null } catch (ReflectiveOperationException | RuntimeException | LinkageError | GroovyBugError ignored) { - return null; + return null } } - return null; + return null } // ClassNode.getField walks superclasses but not interfaces, so a constant declared on an @@ -2586,50 +2571,50 @@ private String resolveStringConstant(Expression expression, ClassNode declaringC private FieldNode findStaticFinalField(ClassNode owner, String fieldName, Set visited) { for (ClassNode type = owner; type != null; type = type.getSuperClass()) { if (!visited.add(type.getName())) { - return null; + return null } - FieldNode declared = type.getDeclaredField(fieldName); + FieldNode declared = type.getDeclaredField(fieldName) if (declared != null && declared.isStatic() && declared.isFinal()) { - return declared; + return declared } - for (ClassNode implemented : type.getInterfaces()) { - FieldNode inherited = findStaticFinalField(implemented, fieldName, visited); + for (ClassNode implemented in type.getInterfaces()) { + FieldNode inherited = findStaticFinalField(implemented, fieldName, visited) if (inherited != null) { - return inherited; + return inherited } } } - return null; + return null } private void processMethodStatement(ClassNode classNode, ClassNode declaringClass, MethodCallExpression outerCall, MethodCallExpression baseCall, List qualifierCalls, SourceUnit source, Set usedNames) { - List closureCallArgs = flatten(outerCall.getArguments()); + List closureCallArgs = flatten(outerCall.getArguments()) if (closureCallArgs.isEmpty() || !(closureCallArgs.get(closureCallArgs.size() - 1) instanceof ClosureExpression)) { - addError(outerCall, source, "method(...) must end with a body closure: method(\"name\", Type) { ... }"); - return; + addError(outerCall, source, 'method(...) must end with a body closure: method("name", Type) { ... }') + return } - ClosureExpression body = (ClosureExpression) closureCallArgs.get(closureCallArgs.size() - 1); + ClosureExpression body = (ClosureExpression) closureCallArgs.get(closureCallArgs.size() - 1) - List baseArgs = flatten(baseCall.getArguments()); + List baseArgs = flatten(baseCall.getArguments()) if (baseCall == outerCall && !baseArgs.isEmpty()) { - baseArgs = baseArgs.subList(0, baseArgs.size() - 1); + baseArgs = baseArgs.subList(0, baseArgs.size() - 1) } - TypeAndName typeAndName = parseNameAndType(baseArgs, baseCall, source, METHOD_CALL, true, declaringClass); + TypeAndName typeAndName = parseNameAndType(baseArgs, baseCall, source, METHOD_CALL, true, declaringClass) if (typeAndName == null) { - return; + return } if (!registerName(typeAndName.name, baseCall, source, usedNames, - "is already used by another member of the class (declared, inherited, or another field(...)/method(...) statement) - " + - "generated member names must be unique")) { - return; + 'is already used by another member of the class (declared, inherited, or another field(...)/method(...) statement) - ' + + 'generated member names must be unique')) { + return } - ClassNode returnType = declaredType(typeAndName, qualifierCalls, outerCall, true, source, METHOD_CALL); + ClassNode returnType = declaredType(typeAndName, qualifierCalls, outerCall, true, source, METHOD_CALL) if (returnType == null) { - return; + return } MethodNode helperMethod = new MethodNode( @@ -2638,29 +2623,29 @@ private void processMethodStatement(ClassNode classNode, ClassNode declaringClas returnType, body.getParameters() == null ? Parameter.EMPTY_ARRAY : body.getParameters(), ClassNode.EMPTY_ARRAY, - body.getCode()); - helperMethod.setSourcePosition(baseCall); + body.getCode()) + helperMethod.setSourcePosition(baseCall) - for (MethodCallExpression qualifierCall : qualifierCalls) { + for (MethodCallExpression qualifierCall in qualifierCalls) { if (TYPE_ARGUMENTS_CALL.equals(qualifierCall.getMethodAsString())) { - continue; + continue } - List qualifierArgs = flatten(qualifierCall.getArguments()); + List qualifierArgs = flatten(qualifierCall.getArguments()) if (qualifierCall == outerCall) { - qualifierArgs = qualifierArgs.subList(0, qualifierArgs.size() - 1); + qualifierArgs = qualifierArgs.subList(0, qualifierArgs.size() - 1) } if (!applyGenericAnnotation(helperMethod, qualifierCall, qualifierArgs, source)) { - return; + return } } // A helper's body is lifted out of the closure exactly as a bean's is, so it needs the same // correction - always non-static, there being no .staticMethod() for method(...). if (!rehomeAnonymousInnerClasses(helperMethod, classNode, false, typeAndName.name, source)) { - return; + return } - classNode.addMethod(helperMethod); + classNode.addMethod(helperMethod) } /** @@ -2681,66 +2666,66 @@ private void processMethodStatement(ClassNode classNode, ClassNode declaringClas */ private ClassNode declaredType(TypeAndName typeAndName, List qualifierCalls, MethodCallExpression outerCall, boolean outerCallCarriesClosure, SourceUnit source, String callName) { - ClassNode raw = typeAndName.type.getType(); - for (MethodCallExpression qualifierCall : qualifierCalls) { + ClassNode raw = typeAndName.type.getType() + for (MethodCallExpression qualifierCall in qualifierCalls) { if (!TYPE_ARGUMENTS_CALL.equals(qualifierCall.getMethodAsString())) { - continue; + continue } - List args = flatten(qualifierCall.getArguments()); + List args = flatten(qualifierCall.getArguments()) if (outerCallCarriesClosure && qualifierCall == outerCall && !args.isEmpty()) { - args = args.subList(0, args.size() - 1); + args = args.subList(0, args.size() - 1) } - String rawName = raw.getNameWithoutPackage(); + String rawName = raw.getNameWithoutPackage() if (args.isEmpty()) { - addError(qualifierCall, source, ".typeArguments(...) requires at least one type, e.g. " + - callName + "(..., " + rawName + ").typeArguments(String)"); - return null; + addError(qualifierCall, source, '.typeArguments(...) requires at least one type, e.g. ' + + callName + '(..., ' + rawName + ').typeArguments(String)') + return null } - GenericsType[] typeArguments = new GenericsType[args.size()]; + GenericsType[] typeArguments = new GenericsType[args.size()] for (int i = 0; i < args.size(); i++) { if (!(args.get(i) instanceof ClassExpression)) { - addError(args.get(i), source, ".typeArguments(...) takes types, e.g. " + - ".typeArguments(String) or .typeArguments(String, Integer)"); - return null; + addError(args.get(i), source, '.typeArguments(...) takes types, e.g. ' + + '.typeArguments(String) or .typeArguments(String, Integer)') + return null } - typeArguments[i] = new GenericsType(((ClassExpression) args.get(i)).getType()); + typeArguments[i] = new GenericsType(((ClassExpression) args.get(i)).getType()) } // The type parameters the declared type actually has. Checking the count here turns a // mismatch into a located error naming both numbers, rather than an unchecked generic // signature that only misleads whoever reads the bean's type later. - GenericsType[] declared = raw.redirect().getGenericsTypes(); + GenericsType[] declared = raw.redirect().getGenericsTypes() if (declared == null || declared.length == 0) { - addError(qualifierCall, source, rawName + " is not a generic type, so it has no type " + - "arguments to give"); - return null; + addError(qualifierCall, source, rawName + ' is not a generic type, so it has no type ' + + 'arguments to give') + return null } if (declared.length != typeArguments.length) { - addError(qualifierCall, source, rawName + " declares " + declared.length + - " type parameter" + (declared.length == 1 ? "" : "s") + ", so .typeArguments(...) takes " + - declared.length + ", not " + typeArguments.length); - return null; + addError(qualifierCall, source, rawName + ' declares ' + declared.length + + ' type parameter' + (declared.length == 1 ? '' : 's') + ', so .typeArguments(...) takes ' + + declared.length + ', not ' + typeArguments.length) + return null } - return GenericsUtils.makeClassSafeWithGenerics(raw, typeArguments); + return GenericsUtils.makeClassSafeWithGenerics(raw, typeArguments) } - return raw; + return raw } private static final class TypeAndName { - private final ClassExpression type; - private final String name; + private final ClassExpression type + private final String name /** The type actually constructed, when stated separately from the declared type; else null. */ - private final ClassExpression implementation; + private final ClassExpression implementation TypeAndName(ClassExpression type, String name, ClassExpression implementation) { - this.type = type; - this.name = name; - this.implementation = implementation; + this.type = type + this.name = name + this.implementation = implementation } } private TypeAndName parseNameAndType(List args, MethodCallExpression call, SourceUnit source, String callName, boolean requireValidIdentifier, ClassNode declaringClass) { - return parseNameAndType(args, call, source, callName, requireValidIdentifier, false, declaringClass); + return parseNameAndType(args, call, source, callName, requireValidIdentifier, false, declaringClass) } /** @@ -2757,110 +2742,110 @@ private TypeAndName parseNameAndType(List args, MethodCallExpression ClassNode declaringClass) { // Two trailing type literals mean the second is the implementation. Split it off first so // everything below reads the same [name, ] Type head it always did. - ClassExpression implementation = null; + ClassExpression implementation = null if (allowImplementation && namesImplementation(args)) { - implementation = (ClassExpression) args.get(args.size() - 1); - args = withoutImplementation(args); + implementation = (ClassExpression) args.get(args.size() - 1) + args = withoutImplementation(args) } - int maxArgs = 2; + int maxArgs = 2 if (args.isEmpty() || args.size() > maxArgs || !(args.get(args.size() - 1) instanceof ClassExpression)) { if (args.size() == 2 && args.get(0) instanceof ClassExpression) { - addError(call, source, callName + "(...) takes the name before the type: " + - callName + "(\"myGreeter\", Greeter), not " + callName + "(Greeter, \"myGreeter\")"); + addError(call, source, callName + '(...) takes the name before the type: ' + + callName + '("myGreeter", Greeter), not ' + callName + '(Greeter, "myGreeter")') } else { - addError(call, source, callName + "(...) requires a type, optionally preceded by a name, " + - "e.g. " + callName + "(Greeter) or " + callName + "(\"myGreeter\", Greeter)"); + addError(call, source, callName + '(...) requires a type, optionally preceded by a name, ' + + 'e.g. ' + callName + '(Greeter) or ' + callName + '("myGreeter", Greeter)') } - return null; + return null } - ClassExpression type = (ClassExpression) args.get(args.size() - 1); + ClassExpression type = (ClassExpression) args.get(args.size() - 1) - String name; + String name if (args.size() == 1) { - name = decapitalize(simpleName(type.getType())); + name = decapitalize(simpleName(type.getType())) // The derived name becomes the generated member's name just as an explicit one does, so // it has to clear the same bar - decapitalizing Boolean, Long or Class hands back a Java // keyword, and no closure body could then reference the member. if (requireValidIdentifier && !isValidJavaIdentifier(name)) { - addError(call, source, "\"" + name + "\", derived from " + - type.getType().getNameWithoutPackage() + ", is not a valid name: it becomes the " + + addError(call, source, '"' + name + '", derived from ' + + type.getType().getNameWithoutPackage() + ', is not a valid name: it becomes the ' + "generated member's name, so it must be a valid Java identifier - give one " + - "explicitly, e.g. " + callName + "(\"" + name + "Value\", " + - type.getType().getNameWithoutPackage() + ")"); - return null; + 'explicitly, e.g. ' + callName + '("' + name + 'Value", ' + + type.getType().getNameWithoutPackage() + ')') + return null } } else { - Expression nameArg = args.get(0); + Expression nameArg = args.get(0) // A literal or any compile-time String constant - the same folding .value(...) does for a // config key, and for the same reason. A bean name is often already a constant, because // something else has to look the bean up by it; making the DSL the one place that cannot // say the constant's name would mean writing the string twice and letting the two drift. - String nameValue = resolveStringConstant(nameArg, declaringClass); + String nameValue = resolveStringConstant(nameArg, declaringClass) if (nameValue == null) { - addError(nameArg, source, callName + "(name, Type) requires the name to be a String literal " + - "or a compile-time String constant, e.g. " + callName + "(\"myGreeter\", Greeter) or " + - callName + "(VIEW_LOADER_BEAN, GroovyPageResourceLoader)"); - return null; + addError(nameArg, source, callName + '(name, Type) requires the name to be a String literal ' + + 'or a compile-time String constant, e.g. ' + callName + '("myGreeter", Greeter) or ' + + callName + '(VIEW_LOADER_BEAN, GroovyPageResourceLoader)') + return null } - name = nameValue; + name = nameValue if (requireValidIdentifier && !isValidJavaIdentifier(name)) { - addError(nameArg, source, "\"" + name + "\" is not a valid name: it becomes the generated " + - "member's name, so it must be a valid Java identifier"); - return null; + addError(nameArg, source, '"' + name + '" is not a valid name: it becomes the generated ' + + "member's name, so it must be a valid Java identifier") + return null } // Even bean(...), which otherwise allows any Spring name, must reject a blank one: // Spring treats a blank @Bean name as absent and falls back to the method name, so // the name actually written would be silently discarded. if (!requireValidIdentifier && name.isBlank()) { - addError(nameArg, source, callName + "(name, Type) requires a non-blank name"); - return null; + addError(nameArg, source, callName + '(name, Type) requires a non-blank name') + return null } } - return new TypeAndName(type, name, implementation); + return new TypeAndName(type, name, implementation) } private boolean applyQualifier(MethodNode beanMethod, String beanName, MethodCallExpression qualifierCall, List args, SourceUnit source) { - String name = qualifierCall.getMethodAsString(); + String name = qualifierCall.getMethodAsString() // Consumed by declaredType(...) before the method node existed - it shapes the declared type // rather than attaching anything to the member. if (TYPE_ARGUMENTS_CALL.equals(name)) { - return true; + return true } if (CONDITIONAL_ON_BEAN_CALL.equals(name)) { - AnnotationNode annotation = conditionalOnBeanAnnotation(args, qualifierCall, source); - return annotation != null && addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source); + AnnotationNode annotation = conditionalOnBeanAnnotation(args, qualifierCall, source) + return annotation != null && addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source) } if (CONDITIONAL_ON_MISSING_BEAN_CALL.equals(name)) { - AnnotationNode annotation = conditionalOnMissingBeanAnnotation(args, qualifierCall, source); - return annotation != null && addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source); + AnnotationNode annotation = conditionalOnMissingBeanAnnotation(args, qualifierCall, source) + return annotation != null && addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source) } if (CONDITIONAL_ON_PROPERTY_CALL.equals(name)) { - AnnotationNode annotation = conditionalOnPropertyAnnotation(args, qualifierCall, source); - return annotation != null && addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source); + AnnotationNode annotation = conditionalOnPropertyAnnotation(args, qualifierCall, source) + return annotation != null && addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source) } if (CONDITIONAL_ON_EXPRESSION_CALL.equals(name)) { - AnnotationNode annotation = conditionalOnExpressionAnnotation(args, qualifierCall, source); - return annotation != null && addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source); + AnnotationNode annotation = conditionalOnExpressionAnnotation(args, qualifierCall, source) + return annotation != null && addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source) } if (CONDITIONAL_ON_CLASS_CALL.equals(name)) { - AnnotationNode annotation = conditionalOnClassAnnotation(args, qualifierCall, source); - return annotation != null && addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source); + AnnotationNode annotation = conditionalOnClassAnnotation(args, qualifierCall, source) + return annotation != null && addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source) } if (CONDITIONAL_ON_MISSING_BEAN_NAME_CALL.equals(name)) { - AnnotationNode annotation = conditionalOnMissingBeanNameAnnotation(args, beanName, qualifierCall, source); - return annotation != null && addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source); + AnnotationNode annotation = conditionalOnMissingBeanNameAnnotation(args, beanName, qualifierCall, source) + return annotation != null && addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source) } if (PRIMARY_CALL.equals(name) || LAZY_CALL.equals(name)) { if (!args.isEmpty()) { - addError(qualifierCall, source, "." + name + "() takes no arguments"); - return false; + addError(qualifierCall, source, '.' + name + '() takes no arguments') + return false } - Class annotationType = PRIMARY_CALL.equals(name) ? Primary.class : Lazy.class; + Class annotationType = PRIMARY_CALL.equals(name) ? Primary : Lazy return addAnnotationIfAbsent(beanMethod, qualifierCall, - new AnnotationNode(ClassHelper.make(annotationType)), source); + new AnnotationNode(ClassHelper.make(annotationType)), source) } // Generates a static factory method - Spring's recommended shape for a // BeanFactoryPostProcessor/BeanPostProcessor bean, which must be creatable without @@ -2868,22 +2853,22 @@ private boolean applyQualifier(MethodNode beanMethod, String beanName, MethodCal // field(...)/method(...) members, which are instance members of that class. if (STATIC_METHOD_CALL.equals(name)) { if (!args.isEmpty()) { - addError(qualifierCall, source, ".staticMethod() takes no arguments"); - return false; + addError(qualifierCall, source, '.staticMethod() takes no arguments') + return false } - beanMethod.setModifiers(beanMethod.getModifiers() | Modifier.STATIC); - return true; + beanMethod.setModifiers(beanMethod.getModifiers() | Modifier.STATIC) + return true } if (SCOPE_CALL.equals(name)) { - return applyScopeQualifier(beanMethod, qualifierCall, args, source); + return applyScopeQualifier(beanMethod, qualifierCall, args, source) } if (ALIASES_CALL.equals(name)) { - return applyAliasesQualifier(beanMethod, beanName, qualifierCall, args, source); + return applyAliasesQualifier(beanMethod, beanName, qualifierCall, args, source) } if (CONDITIONAL_ON_GRAILS_ENV_CALL.equals(name)) { - return applyConditionalOnGrailsEnvQualifier(beanMethod, qualifierCall, args, source); + return applyConditionalOnGrailsEnvQualifier(beanMethod, qualifierCall, args, source) } - return applyGenericAnnotation(beanMethod, qualifierCall, args, source); + return applyGenericAnnotation(beanMethod, qualifierCall, args, source) } /** @@ -2902,42 +2887,42 @@ private boolean applyQualifier(MethodNode beanMethod, String beanName, MethodCal private boolean applyAliasesQualifier(MethodNode beanMethod, String beanName, MethodCallExpression qualifierCall, List args, SourceUnit source) { if (args.isEmpty()) { - addError(qualifierCall, source, ".aliases(...) requires at least one additional name, " + - "e.g. .aliases(\"legacyName\")"); - return false; + addError(qualifierCall, source, '.aliases(...) requires at least one additional name, ' + + 'e.g. .aliases("legacyName")') + return false } - List existing = beanMethod.getAnnotations(ClassHelper.make(Bean.class)); + List existing = beanMethod.getAnnotations(ClassHelper.make(Bean)) if (existing.isEmpty()) { - addError(qualifierCall, source, ".aliases(...) applies to bean(...) declarations only"); - return false; + addError(qualifierCall, source, '.aliases(...) applies to bean(...) declarations only') + return false } - Expression namesMember = existing.get(0).getMember("value"); + Expression namesMember = existing.get(0).getMember('value') if (!(namesMember instanceof ListExpression)) { - addError(qualifierCall, source, ".aliases(...) cannot be combined with a @Bean whose names " + - "were set another way"); - return false; - } - ListExpression names = (ListExpression) namesMember; - Set seen = new HashSet<>(); - seen.add(beanName); - for (Expression arg : args) { - Expression folded = foldStringValue(arg); - Object value = folded instanceof ConstantExpression ? ((ConstantExpression) folded).getValue() : null; + addError(qualifierCall, source, '.aliases(...) cannot be combined with a @Bean whose names ' + + 'were set another way') + return false + } + ListExpression names = (ListExpression) namesMember + Set seen = new HashSet<>() + seen.add(beanName) + for (Expression arg in args) { + Expression folded = foldStringValue(arg) + Object value = folded instanceof ConstantExpression ? ((ConstantExpression) folded).getValue() : null if (!(value instanceof String) || ((String) value).isBlank()) { - addError(arg, source, ".aliases(...) takes non-blank names as Strings, " + - "e.g. .aliases(\"legacyName\")"); - return false; + addError(arg, source, '.aliases(...) takes non-blank names as Strings, ' + + 'e.g. .aliases("legacyName")') + return false } - String alias = (String) value; + String alias = (String) value if (!seen.add(alias)) { addError(arg, source, alias.equals(beanName) ? - "\"" + alias + "\" is this bean's own name, so it is not an alias of anything" : - "\"" + alias + "\" is given as an alias twice"); - return false; + '"' + alias + "\" is this bean's own name, so it is not an alias of anything" : + '"' + alias + '" is given as an alias twice') + return false } - names.addExpression(folded); + names.addExpression(folded) } - return true; + return true } /** @@ -2957,41 +2942,41 @@ private boolean applyAliasesQualifier(MethodNode beanMethod, String beanName, */ private boolean applyScopeQualifier(MethodNode beanMethod, MethodCallExpression qualifierCall, List args, SourceUnit source) { - MapExpression members = !args.isEmpty() && args.get(0) instanceof MapExpression ? (MapExpression) args.get(0) : null; - List positional = members != null ? args.subList(1, args.size()) : args; + MapExpression members = !args.isEmpty() && args.get(0) instanceof MapExpression ? (MapExpression) args.get(0) : null + List positional = members != null ? args.subList(1, args.size()) : args if (positional.size() > 1) { - addError(qualifierCall, source, ".scope(...) takes one scope name, e.g. " + - ".scope(\"session\", proxyMode: ScopedProxyMode.TARGET_CLASS)"); - return false; + addError(qualifierCall, source, '.scope(...) takes one scope name, e.g. ' + + '.scope("session", proxyMode: ScopedProxyMode.TARGET_CLASS)') + return false } if (positional.isEmpty() && !namesAnyOf(members, SCOPE_NAME_MEMBERS)) { - addError(qualifierCall, source, ".scope(...) needs a scope name, positionally or as " + - "value:/scopeName: - e.g. .scope(\"prototype\")"); - return false; + addError(qualifierCall, source, '.scope(...) needs a scope name, positionally or as ' + + 'value:/scopeName: - e.g. .scope("prototype")') + return false } if (!positional.isEmpty() && rejectPositionalAndNamed(members, SCOPE_NAME_MEMBERS, SCOPE_CALL, - "a scope name", qualifierCall, source)) { - return false; + 'a scope name', qualifierCall, source)) { + return false } - AnnotationNode scopeAnnotation = new AnnotationNode(ClassHelper.make(Scope.class)); + AnnotationNode scopeAnnotation = new AnnotationNode(ClassHelper.make(Scope)) if (members != null && !addMembersFromMap(scopeAnnotation, members, qualifierCall, source)) { - return false; + return false } if (!positional.isEmpty()) { - Expression folded = foldStringValue(positional.get(0)); - Object scopeValue = folded instanceof ConstantExpression ? ((ConstantExpression) folded).getValue() : null; + Expression folded = foldStringValue(positional.get(0)) + Object scopeValue = folded instanceof ConstantExpression ? ((ConstantExpression) folded).getValue() : null if (!(scopeValue instanceof String) || ((String) scopeValue).isEmpty()) { - addError(qualifierCall, source, ".scope(...) requires a non-empty String scope name, " + - "e.g. .scope(\"prototype\")"); - return false; + addError(qualifierCall, source, '.scope(...) requires a non-empty String scope name, ' + + 'e.g. .scope("prototype")') + return false } - scopeAnnotation.setMember("value", folded); + scopeAnnotation.setMember('value', folded) } - return addAnnotationIfAbsent(beanMethod, qualifierCall, scopeAnnotation, source); + return addAnnotationIfAbsent(beanMethod, qualifierCall, scopeAnnotation, source) } /** {@code @Scope}'s attributes that name the scope; aliases of each other. */ - private static final Set SCOPE_NAME_MEMBERS = Set.of("value", "scopeName"); + private static final Set SCOPE_NAME_MEMBERS = Set.of('value', 'scopeName') /** * Compiles {@code .conditionalOnGrailsEnv("development"[, ...])} into {@code @ConditionalOnGrailsEnv}. @@ -3005,49 +2990,49 @@ private boolean applyScopeQualifier(MethodNode beanMethod, MethodCallExpression private boolean applyConditionalOnGrailsEnvQualifier(AnnotatedNode beanMethod, MethodCallExpression qualifierCall, List args, SourceUnit source) { if (args.isEmpty()) { - addError(qualifierCall, source, ".conditionalOnGrailsEnv(...) requires at least one environment name, " + - "e.g. .conditionalOnGrailsEnv(\"development\")"); - return false; - } - ListExpression names = new ListExpression(); - for (Expression arg : args) { - Expression folded = foldStringValue(arg); - Object value = folded instanceof ConstantExpression ? ((ConstantExpression) folded).getValue() : null; + addError(qualifierCall, source, '.conditionalOnGrailsEnv(...) requires at least one environment name, ' + + 'e.g. .conditionalOnGrailsEnv("development")') + return false + } + ListExpression names = new ListExpression() + for (Expression arg in args) { + Expression folded = foldStringValue(arg) + Object value = folded instanceof ConstantExpression ? ((ConstantExpression) folded).getValue() : null if (!(value instanceof String) || ((String) value).isBlank()) { - addError(arg, source, ".conditionalOnGrailsEnv(...) takes non-blank environment names as Strings, " + - "e.g. .conditionalOnGrailsEnv(\"development\", \"test\")"); - return false; + addError(arg, source, '.conditionalOnGrailsEnv(...) takes non-blank environment names as Strings, ' + + 'e.g. .conditionalOnGrailsEnv("development", "test")') + return false } - names.addExpression(folded); + names.addExpression(folded) } - AnnotationNode annotation = new AnnotationNode(ClassHelper.make(ConditionalOnGrailsEnv.class)); - annotation.setMember("value", names); - return addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source); + AnnotationNode annotation = new AnnotationNode(ClassHelper.make(ConditionalOnGrailsEnv)) + annotation.setMember('value', names) + return addAnnotationIfAbsent(beanMethod, qualifierCall, annotation, source) } private boolean applyGenericAnnotation(AnnotatedNode target, MethodCallExpression qualifierCall, List args, SourceUnit source) { - MapExpression members = !args.isEmpty() && args.get(0) instanceof MapExpression ? (MapExpression) args.get(0) : null; - List remaining = members != null ? args.subList(1, args.size()) : args; + MapExpression members = !args.isEmpty() && args.get(0) instanceof MapExpression ? (MapExpression) args.get(0) : null + List remaining = members != null ? args.subList(1, args.size()) : args if (remaining.size() != 1 || !(remaining.get(0) instanceof ClassExpression)) { - addError(qualifierCall, source, ".annotate(...) requires an annotation type as its only " + - "positional argument, e.g. .annotate(Order, value: 1) or .annotate(ConditionalOnWebApplication)"); - return false; + addError(qualifierCall, source, '.annotate(...) requires an annotation type as its only ' + + 'positional argument, e.g. .annotate(Order, value: 1) or .annotate(ConditionalOnWebApplication)') + return false } - ClassNode annotationType = ((ClassExpression) remaining.get(0)).getType(); + ClassNode annotationType = ((ClassExpression) remaining.get(0)).getType() if (!annotationType.isAnnotationDefinition()) { - addError(qualifierCall, source, "\"" + annotationType.getName() + "\" is not an annotation type"); - return false; + addError(qualifierCall, source, '"' + annotationType.getName() + '" is not an annotation type') + return false } // Spring never reads a field(...) or method(...) member as a bean, so @Bean on one is a // mistake rather than something to merge into a @Bean that was never attached. - if (annotationType.getName().equals(Bean.class.getName()) && - target.getAnnotations(ClassHelper.make(Bean.class)).isEmpty()) { - addError(qualifierCall, source, ".annotate(Bean, ...) applies to bean(...) declarations - " + - "field(...) and method(...) declare plain members, which Spring never reads as beans"); - return false; + if (annotationType.getName().equals(Bean.getName()) && + target.getAnnotations(ClassHelper.make(Bean)).isEmpty()) { + addError(qualifierCall, source, '.annotate(Bean, ...) applies to bean(...) declarations - ' + + 'field(...) and method(...) declare plain members, which Spring never reads as beans') + return false } // An annotation a QUALIFIER attached is merged into, not collided with: a qualifier sets only @@ -3056,22 +3041,22 @@ private boolean applyGenericAnnotation(AnnotatedNode target, MethodCallExpressio // earlier .annotate(...) attached is a different matter: .annotate(...) already takes every // attribute at once, so writing it twice for one type is a mistake, not an addition, and // still reports as one. - List existing = target.getAnnotations(annotationType); + List existing = target.getAnnotations(annotationType) if (!existing.isEmpty() && existing.get(0).getNodeMetaData(ANNOTATE_ATTACHED) == null) { - return mergeIntoAnnotation(existing.get(0), annotationType, members, qualifierCall, source); + return mergeIntoAnnotation(existing.get(0), annotationType, members, qualifierCall, source) } - AnnotationNode annotation = new AnnotationNode(annotationType); - annotation.putNodeMetaData(ANNOTATE_ATTACHED, Boolean.TRUE); + AnnotationNode annotation = new AnnotationNode(annotationType) + annotation.putNodeMetaData(ANNOTATE_ATTACHED, Boolean.TRUE) if (members != null && !addMembersFromMap(annotation, members, qualifierCall, source)) { - return false; + return false } - return addAnnotationIfAbsent(target, qualifierCall, annotation, source); + return addAnnotationIfAbsent(target, qualifierCall, annotation, source) } /** Marks an annotation node as one {@code .annotate(...)} attached, rather than a qualifier. */ private static final String ANNOTATE_ATTACHED = - GrailsBeansASTTransformation.class.getName() + ".annotateAttached"; + GrailsBeansASTTransformation.getName() + '.annotateAttached' /** * Attributes that alias one another, so setting one when the other is already set is the same @@ -3079,9 +3064,9 @@ private boolean applyGenericAnnotation(AnnotatedNode target, MethodCallExpressio * why it is worth naming rather than leaving to Spring's runtime "different values" error. */ private static final Map> ALIASED_ANNOTATION_MEMBERS = Map.of( - Bean.class.getName(), Set.of("value", "name"), - Scope.class.getName(), Set.of("value", "scopeName"), - ConditionalOnProperty.class.getName(), Set.of("value", "name")); + Bean.getName(), Set.of('value', 'name'), + Scope.getName(), Set.of('value', 'scopeName'), + ConditionalOnProperty.getName(), Set.of('value', 'name')) /** * Folds {@code .annotate(X, ...)} attributes into an {@code @X} a qualifier already attached. @@ -3099,69 +3084,69 @@ private boolean applyGenericAnnotation(AnnotatedNode target, MethodCallExpressio */ private boolean mergeIntoAnnotation(AnnotationNode annotation, ClassNode annotationType, MapExpression members, MethodCallExpression qualifierCall, SourceUnit source) { - String name = annotationType.getNameWithoutPackage(); + String name = annotationType.getNameWithoutPackage() if (members == null || members.getMapEntryExpressions().isEmpty()) { - addError(qualifierCall, source, ".annotate(" + name + ") adds nothing, since @" + name + - " is already attached here - give the attributes to set, e.g. " + - ".annotate(Bean, destroyMethod: \"\")"); - return false; + addError(qualifierCall, source, '.annotate(' + name + ') adds nothing, since @' + name + + ' is already attached here - give the attributes to set, e.g. ' + + '.annotate(Bean, destroyMethod: "")') + return false } - Set aliases = ALIASED_ANNOTATION_MEMBERS.getOrDefault(annotationType.getName(), Set.of()); - for (MapEntryExpression entry : members.getMapEntryExpressions()) { + Set aliases = ALIASED_ANNOTATION_MEMBERS.getOrDefault(annotationType.getName(), Set.of()) + for (MapEntryExpression entry in members.getMapEntryExpressions()) { Object keyValue = entry.getKeyExpression() instanceof ConstantExpression ? - ((ConstantExpression) entry.getKeyExpression()).getValue() : null; + ((ConstantExpression) entry.getKeyExpression()).getValue() : null if (!(keyValue instanceof String)) { - addError(qualifierCall, source, ".annotate(...) attribute names must be simple " + - "identifiers, e.g. .annotate(Bean, destroyMethod: \"\")"); - return false; + addError(qualifierCall, source, '.annotate(...) attribute names must be simple ' + + 'identifiers, e.g. .annotate(Bean, destroyMethod: "")') + return false } - String key = (String) keyValue; - String taken = takenMember(annotation, key, aliases); + String key = (String) keyValue + String taken = takenMember(annotation, key, aliases) if (taken != null) { - if (annotationType.getName().equals(Bean.class.getName()) && aliases.contains(taken)) { + if (annotationType.getName().equals(Bean.getName()) && aliases.contains(taken)) { addError(qualifierCall, source, "a bean's name comes from bean(\"name\", Type), so " + - ".annotate(Bean, " + key + ": ...) would state it twice - rename it there instead"); + '.annotate(Bean, ' + key + ': ...) would state it twice - rename it there instead') } else { - addError(qualifierCall, source, "@" + name + "'s \"" + taken + "\" is already set here" + - (taken.equals(key) ? "" : ", and \"" + key + "\" aliases it") + - " - set it where it is stated, not a second time"); + addError(qualifierCall, source, '@' + name + "'s \"" + taken + '" is already set here' + + (taken.equals(key) ? '' : ', and "' + key + '" aliases it') + + ' - set it where it is stated, not a second time') } - return false; + return false } - annotation.setMember(key, foldStringValue(entry.getValueExpression())); + annotation.setMember(key, foldStringValue(entry.getValueExpression())) } - return true; + return true } /** The member already carrying {@code key}'s value - {@code key} itself, or an alias of it. */ private String takenMember(AnnotationNode annotation, String key, Set aliases) { if (annotation.getMember(key) != null) { - return key; + return key } if (aliases.contains(key)) { - for (String alias : aliases) { + for (String alias in aliases) { if (annotation.getMember(alias) != null) { - return alias; + return alias } } } - return null; + return null } private boolean addMembersFromMap(AnnotationNode annotation, MapExpression members, MethodCallExpression qualifierCall, SourceUnit source) { - for (MapEntryExpression entry : members.getMapEntryExpressions()) { + for (MapEntryExpression entry in members.getMapEntryExpressions()) { Object keyValue = entry.getKeyExpression() instanceof ConstantExpression ? - ((ConstantExpression) entry.getKeyExpression()).getValue() : null; + ((ConstantExpression) entry.getKeyExpression()).getValue() : null if (!(keyValue instanceof String)) { - addError(qualifierCall, source, "." + qualifierCall.getMethodAsString() + - "(...) attribute names must be simple identifiers, e.g. .annotate(Order, value: 1)"); - return false; + addError(qualifierCall, source, '.' + qualifierCall.getMethodAsString() + + '(...) attribute names must be simple identifiers, e.g. .annotate(Order, value: 1)') + return false } - annotation.setMember((String) keyValue, foldStringValue(entry.getValueExpression())); + annotation.setMember((String) keyValue, foldStringValue(entry.getValueExpression())) } - return true; + return true } // Same reason .value(...) folds its arguments: an attribute written as a concatenation is a @@ -3171,46 +3156,46 @@ private boolean addMembersFromMap(AnnotationNode annotation, MapExpression membe // that is not a resolvable String - class literals, numbers, enum constants, arrays - alone. private Expression foldStringValue(Expression value) { if (value instanceof ConstantExpression) { - return value; + return value } - String resolved = resolveStringConstant(value); - return resolved != null ? new ConstantExpression(resolved) : value; + String resolved = resolveStringConstant(value) + return resolved != null ? new ConstantExpression(resolved) : value } private boolean addAnnotationIfAbsent(AnnotatedNode target, MethodCallExpression qualifierCall, AnnotationNode annotation, SourceUnit source) { - ClassNode type = annotation.getClassNode(); + ClassNode type = annotation.getClassNode() if (!target.getAnnotations(type).isEmpty()) { - addError(qualifierCall, source, "@" + type.getNameWithoutPackage() + " is already attached " + - "here, via an earlier qualifier or .annotate(...)"); - return false; + addError(qualifierCall, source, '@' + type.getNameWithoutPackage() + ' is already attached ' + + 'here, via an earlier qualifier or .annotate(...)') + return false } - target.addAnnotation(withPosition(annotation, qualifierCall)); - return true; + target.addAnnotation(withPosition(annotation, qualifierCall)) + return true } private AnnotationNode withPosition(AnnotationNode annotation, ASTNode origin) { - annotation.setSourcePosition(origin); - return annotation; + annotation.setSourcePosition(origin) + return annotation } private List flatten(Expression arguments) { - List result = new ArrayList<>(); + List result = new ArrayList<>() if (arguments instanceof org.codehaus.groovy.ast.expr.TupleExpression) { - result.addAll(((org.codehaus.groovy.ast.expr.TupleExpression) arguments).getExpressions()); + result.addAll(((org.codehaus.groovy.ast.expr.TupleExpression) arguments).getExpressions()) } else { - result.add(arguments); + result.add(arguments) } - return result; + return result } private AnnotationNode beanAnnotation(String beanName) { - AnnotationNode annotation = new AnnotationNode(ClassHelper.make(Bean.class)); - ListExpression names = new ListExpression(); - names.addExpression(new ConstantExpression(beanName)); - annotation.setMember("value", names); - return annotation; + AnnotationNode annotation = new AnnotationNode(ClassHelper.make(Bean)) + ListExpression names = new ListExpression() + names.addExpression(new ConstantExpression(beanName)) + annotation.setMember('value', names) + return annotation } // Zero args -> a bare annotation, letting Spring infer the back-off type from the method's @@ -3219,8 +3204,8 @@ private AnnotationNode beanAnnotation(String beanName) { // generic .annotate(...) escape hatch. private AnnotationNode conditionalOnMissingBeanAnnotation(List args, MethodCallExpression qualifierCall, SourceUnit source) { - return beanConditionAnnotation(ConditionalOnMissingBean.class, CONDITIONAL_ON_MISSING_BEAN_CALL, - args, qualifierCall, source); + return beanConditionAnnotation(ConditionalOnMissingBean, CONDITIONAL_ON_MISSING_BEAN_CALL, + args, qualifierCall, source) } /** @@ -3236,14 +3221,14 @@ private AnnotationNode conditionalOnMissingBeanAnnotation(List args, private AnnotationNode conditionalOnBeanAnnotation(List args, MethodCallExpression qualifierCall, SourceUnit source) { if (args.isEmpty()) { - addError(qualifierCall, source, ".conditionalOnBean() needs at least one type or attribute. With " + + addError(qualifierCall, source, '.conditionalOnBean() needs at least one type or attribute. With ' + "none, Spring deduces the type from this bean's own return type, so the condition reads " + - "\"register this bean only when a bean of its type already exists\" - name what it actually " + - "depends on, e.g. .conditionalOnBean(SmsTransport)"); - return null; + '"register this bean only when a bean of its type already exists" - name what it actually ' + + 'depends on, e.g. .conditionalOnBean(SmsTransport)') + return null } - return beanConditionAnnotation(ConditionalOnBean.class, CONDITIONAL_ON_BEAN_CALL, - args, qualifierCall, source); + return beanConditionAnnotation(ConditionalOnBean, CONDITIONAL_ON_BEAN_CALL, + args, qualifierCall, source) } /** @@ -3261,37 +3246,37 @@ private AnnotationNode conditionalOnBeanAnnotation(List args, */ private AnnotationNode conditionalOnPropertyAnnotation(List args, MethodCallExpression qualifierCall, SourceUnit source) { - MapExpression members = !args.isEmpty() && args.get(0) instanceof MapExpression ? (MapExpression) args.get(0) : null; - List names = members != null ? args.subList(1, args.size()) : args; + MapExpression members = !args.isEmpty() && args.get(0) instanceof MapExpression ? (MapExpression) args.get(0) : null + List names = members != null ? args.subList(1, args.size()) : args if (names.isEmpty() && !namesAnyOf(members, PROPERTY_NAME_MEMBERS)) { - addError(qualifierCall, source, ".conditionalOnProperty(...) needs at least one property name, " + - "positionally or as name:/value: - e.g. .conditionalOnProperty(\"app.offline\", " + - "havingValue: \"false\")"); - return null; + addError(qualifierCall, source, '.conditionalOnProperty(...) needs at least one property name, ' + + 'positionally or as name:/value: - e.g. .conditionalOnProperty("app.offline", ' + + 'havingValue: "false")') + return null } if (!names.isEmpty() && rejectPositionalAndNamed(members, PROPERTY_NAME_MEMBERS, - CONDITIONAL_ON_PROPERTY_CALL, "property names", qualifierCall, source)) { - return null; + CONDITIONAL_ON_PROPERTY_CALL, 'property names', qualifierCall, source)) { + return null } - AnnotationNode annotation = new AnnotationNode(ClassHelper.make(ConditionalOnProperty.class)); + AnnotationNode annotation = new AnnotationNode(ClassHelper.make(ConditionalOnProperty)) if (members != null && !addMembersFromMap(annotation, members, qualifierCall, source)) { - return null; + return null } if (!names.isEmpty()) { - ListExpression nameList = new ListExpression(); - for (Expression name : names) { - Expression folded = foldStringValue(name); - Object value = folded instanceof ConstantExpression ? ((ConstantExpression) folded).getValue() : null; + ListExpression nameList = new ListExpression() + for (Expression name in names) { + Expression folded = foldStringValue(name) + Object value = folded instanceof ConstantExpression ? ((ConstantExpression) folded).getValue() : null if (!(value instanceof String) || ((String) value).isBlank()) { - addError(name, source, ".conditionalOnProperty(...) takes non-blank property names as " + - "Strings, e.g. .conditionalOnProperty(\"app.offline\")"); - return null; + addError(name, source, '.conditionalOnProperty(...) takes non-blank property names as ' + + 'Strings, e.g. .conditionalOnProperty("app.offline")') + return null } - nameList.addExpression(folded); + nameList.addExpression(folded) } - annotation.setMember("name", nameList); + annotation.setMember('name', nameList) } - return annotation; + return annotation } /** @@ -3308,21 +3293,21 @@ private AnnotationNode conditionalOnPropertyAnnotation(List args, */ private AnnotationNode conditionalOnExpressionAnnotation(List args, MethodCallExpression qualifierCall, SourceUnit source) { - Expression folded = args.size() == 1 ? foldStringValue(args.get(0)) : null; - Object value = folded instanceof ConstantExpression ? ((ConstantExpression) folded).getValue() : null; + Expression folded = args.size() == 1 ? foldStringValue(args.get(0)) : null + Object value = folded instanceof ConstantExpression ? ((ConstantExpression) folded).getValue() : null if (!(value instanceof String) || ((String) value).isBlank()) { - addError(qualifierCall, source, ".conditionalOnExpression(...) takes exactly one non-blank " + - "String expression, e.g. .conditionalOnExpression('${app.offline:false}')"); - return null; + addError(qualifierCall, source, '.conditionalOnExpression(...) takes exactly one non-blank ' + + "String expression, e.g. .conditionalOnExpression('\${app.offline:false}')") + return null } - AnnotationNode annotation = new AnnotationNode(ClassHelper.make(ConditionalOnExpression.class)); - annotation.setMember("value", folded); - return annotation; + AnnotationNode annotation = new AnnotationNode(ClassHelper.make(ConditionalOnExpression)) + annotation.setMember('value', folded) + return annotation } /** The attributes of {@code @ConditionalOnProperty} that name properties; aliases of each other. */ - private static final Set CLASS_TYPE_MEMBERS = Set.of("value"); - private static final Set CLASS_NAME_MEMBERS = Set.of("name"); + private static final Set CLASS_TYPE_MEMBERS = Set.of('value') + private static final Set CLASS_NAME_MEMBERS = Set.of('name') /** * {@code @ConditionalOnClass} - "register this only when that class is on the classpath", the @@ -3344,67 +3329,67 @@ private AnnotationNode conditionalOnExpressionAnnotation(List args, */ private AnnotationNode conditionalOnClassAnnotation(List args, MethodCallExpression qualifierCall, SourceUnit source) { - MapExpression members = !args.isEmpty() && args.get(0) instanceof MapExpression ? (MapExpression) args.get(0) : null; - List positional = members != null ? args.subList(1, args.size()) : args; + MapExpression members = !args.isEmpty() && args.get(0) instanceof MapExpression ? (MapExpression) args.get(0) : null + List positional = members != null ? args.subList(1, args.size()) : args if (positional.isEmpty() && !namesAnyOf(members, CLASS_TYPE_MEMBERS) && !namesAnyOf(members, CLASS_NAME_MEMBERS)) { - addError(qualifierCall, source, ".conditionalOnClass(...) needs at least one class, as a type or " + - "a String name - e.g. .conditionalOnClass(MongoLockProvider) for a class this module " + - "compiles against, or .conditionalOnClass(name: \"net.javacrumbs.shedlock.provider.mongo." + - "MongoLockProvider\") for one that may be absent"); - return null; - } - ListExpression types = new ListExpression(); - ListExpression names = new ListExpression(); - for (Expression arg : positional) { + addError(qualifierCall, source, '.conditionalOnClass(...) needs at least one class, as a type or ' + + 'a String name - e.g. .conditionalOnClass(MongoLockProvider) for a class this module ' + + 'compiles against, or .conditionalOnClass(name: "net.javacrumbs.shedlock.provider.mongo.' + + 'MongoLockProvider") for one that may be absent') + return null + } + ListExpression types = new ListExpression() + ListExpression names = new ListExpression() + for (Expression arg in positional) { if (arg instanceof ClassExpression) { - types.addExpression(arg); - continue; + types.addExpression(arg) + continue } - Expression folded = foldStringValue(arg); - Object value = folded instanceof ConstantExpression ? ((ConstantExpression) folded).getValue() : null; + Expression folded = foldStringValue(arg) + Object value = folded instanceof ConstantExpression ? ((ConstantExpression) folded).getValue() : null if (!(value instanceof String) || ((String) value).isBlank()) { - addError(arg, source, ".conditionalOnClass(...) takes types or non-blank String class names, " + - "e.g. .conditionalOnClass(MongoLockProvider) or " + - ".conditionalOnClass(\"net.javacrumbs.shedlock.provider.mongo.MongoLockProvider\")"); - return null; + addError(arg, source, '.conditionalOnClass(...) takes types or non-blank String class names, ' + + 'e.g. .conditionalOnClass(MongoLockProvider) or ' + + '.conditionalOnClass("net.javacrumbs.shedlock.provider.mongo.MongoLockProvider")') + return null } - names.addExpression(folded); + names.addExpression(folded) } if (!types.getExpressions().isEmpty() && rejectPositionalAndNamed(members, CLASS_TYPE_MEMBERS, - CONDITIONAL_ON_CLASS_CALL, "types", qualifierCall, source)) { - return null; + CONDITIONAL_ON_CLASS_CALL, 'types', qualifierCall, source)) { + return null } if (!names.getExpressions().isEmpty() && rejectPositionalAndNamed(members, CLASS_NAME_MEMBERS, - CONDITIONAL_ON_CLASS_CALL, "class names", qualifierCall, source)) { - return null; + CONDITIONAL_ON_CLASS_CALL, 'class names', qualifierCall, source)) { + return null } - AnnotationNode annotation = new AnnotationNode(ClassHelper.make(ConditionalOnClass.class)); + AnnotationNode annotation = new AnnotationNode(ClassHelper.make(ConditionalOnClass)) if (members != null && !addMembersFromMap(annotation, members, qualifierCall, source)) { - return null; + return null } if (!types.getExpressions().isEmpty()) { - annotation.setMember("value", types); + annotation.setMember('value', types) } if (!names.getExpressions().isEmpty()) { - annotation.setMember("name", names); + annotation.setMember('name', names) } - return annotation; + return annotation } - private static final Set PROPERTY_NAME_MEMBERS = Set.of("name", "value"); + private static final Set PROPERTY_NAME_MEMBERS = Set.of('name', 'value') private boolean namesAnyOf(MapExpression members, Set keys) { if (members == null) { - return false; + return false } - for (MapEntryExpression entry : members.getMapEntryExpressions()) { + for (MapEntryExpression entry in members.getMapEntryExpressions()) { Object keyValue = entry.getKeyExpression() instanceof ConstantExpression ? - ((ConstantExpression) entry.getKeyExpression()).getValue() : null; + ((ConstantExpression) entry.getKeyExpression()).getValue() : null if (keys.contains(keyValue)) { - return true; + return true } } - return false; + return false } /** @@ -3415,41 +3400,41 @@ private boolean namesAnyOf(MapExpression members, Set keys) { private boolean rejectPositionalAndNamed(MapExpression members, Set aliasMembers, String callName, String what, MethodCallExpression qualifierCall, SourceUnit source) { if (!namesAnyOf(members, aliasMembers)) { - return false; + return false } - addError(qualifierCall, source, callName + "(...) was given " + what + " both positionally and via " + - String.join(":/", aliasMembers.stream().sorted().toList()) + ": - use one or the other"); - return true; + addError(qualifierCall, source, callName + '(...) was given ' + what + ' both positionally and via ' + + String.join(':/', aliasMembers.stream().sorted().toList()) + ': - use one or the other') + return true } // Shared by both: positional types go to value, named arguments are the annotation's own // attributes, and giving types both ways at once is rejected. private AnnotationNode beanConditionAnnotation(Class annotationType, String callName, List args, MethodCallExpression qualifierCall, SourceUnit source) { - AnnotationNode annotation = new AnnotationNode(ClassHelper.make(annotationType)); - MapExpression members = !args.isEmpty() && args.get(0) instanceof MapExpression ? (MapExpression) args.get(0) : null; - List types = members != null ? args.subList(1, args.size()) : args; - if (!types.isEmpty() && rejectPositionalAndNamed(members, Set.of("value"), callName, "types", + AnnotationNode annotation = new AnnotationNode(ClassHelper.make(annotationType)) + MapExpression members = !args.isEmpty() && args.get(0) instanceof MapExpression ? (MapExpression) args.get(0) : null + List types = members != null ? args.subList(1, args.size()) : args + if (!types.isEmpty() && rejectPositionalAndNamed(members, Set.of('value'), callName, 'types', qualifierCall, source)) { - return null; + return null } if (members != null && !addMembersFromMap(annotation, members, qualifierCall, source)) { - return null; + return null } if (!types.isEmpty()) { - ListExpression typeList = new ListExpression(); - for (Expression type : types) { + ListExpression typeList = new ListExpression() + for (Expression type in types) { if (!(type instanceof ClassExpression)) { - addError(qualifierCall, source, callName + "(...) arguments must be types " + - "and/or named attributes, e.g. " + callName + "(Greeter) or " + callName + - "(name: \"greeter\", search: SearchStrategy.CURRENT)"); - continue; + addError(qualifierCall, source, callName + '(...) arguments must be types ' + + 'and/or named attributes, e.g. ' + callName + '(Greeter) or ' + callName + + '(name: "greeter", search: SearchStrategy.CURRENT)') + continue } - typeList.addExpression(type); + typeList.addExpression(type) } - annotation.setMember("value", typeList); + annotation.setMember('value', typeList) } - return annotation; + return annotation } // "Register this bean unless a bean with THIS bean's name already exists" - the name member is @@ -3459,34 +3444,34 @@ private AnnotationNode beanConditionAnnotation(Class annotationType, String c // available on .conditionalOnMissingBean(...). private AnnotationNode conditionalOnMissingBeanNameAnnotation(List args, String beanName, MethodCallExpression qualifierCall, SourceUnit source) { - MapExpression members = !args.isEmpty() && args.get(0) instanceof MapExpression ? (MapExpression) args.get(0) : null; + MapExpression members = !args.isEmpty() && args.get(0) instanceof MapExpression ? (MapExpression) args.get(0) : null if (args.size() > (members != null ? 1 : 0)) { - addError(qualifierCall, source, "conditionalOnMissingBeanName(...) takes only named attributes " + + addError(qualifierCall, source, 'conditionalOnMissingBeanName(...) takes only named attributes ' + "(e.g. search: SearchStrategy.CURRENT) - it always backs off by this bean's own name; " + - "use conditionalOnMissingBean(...) for type-based or fully explicit conditions"); - return null; + 'use conditionalOnMissingBean(...) for type-based or fully explicit conditions') + return null } if (members != null) { - for (MapEntryExpression entry : members.getMapEntryExpressions()) { + for (MapEntryExpression entry in members.getMapEntryExpressions()) { Object keyValue = entry.getKeyExpression() instanceof ConstantExpression ? - ((ConstantExpression) entry.getKeyExpression()).getValue() : null; - if ("name".equals(keyValue) || "value".equals(keyValue)) { - addError(qualifierCall, source, "conditionalOnMissingBeanName(...) sets name automatically " + - "from this bean's own name - use conditionalOnMissingBean(...) to spell out name: or types"); - return null; + ((ConstantExpression) entry.getKeyExpression()).getValue() : null + if ('name'.equals(keyValue) || 'value'.equals(keyValue)) { + addError(qualifierCall, source, 'conditionalOnMissingBeanName(...) sets name automatically ' + + "from this bean's own name - use conditionalOnMissingBean(...) to spell out name: or types") + return null } } } - AnnotationNode annotation = new AnnotationNode(ClassHelper.make(ConditionalOnMissingBean.class)); + AnnotationNode annotation = new AnnotationNode(ClassHelper.make(ConditionalOnMissingBean)) if (members != null && !addMembersFromMap(annotation, members, qualifierCall, source)) { - return null; + return null } - annotation.setMember("name", new ConstantExpression(beanName)); - return annotation; + annotation.setMember('name', new ConstantExpression(beanName)) + return annotation } private String decapitalize(String name) { - return Introspector.decapitalize(name); + return Introspector.decapitalize(name) } /** @@ -3495,35 +3480,35 @@ private String decapitalize(String name) { * {@code Outer$Helper} and so derived a bean name no one would write. */ private static String simpleName(ClassNode type) { - String name = type.getNameWithoutPackage(); - ClassNode outer = type.getOuterClass(); - if (outer != null && name.startsWith(outer.getNameWithoutPackage() + "$")) { - return name.substring(outer.getNameWithoutPackage().length() + 1); + String name = type.getNameWithoutPackage() + ClassNode outer = type.getOuterClass() + if (outer != null && name.startsWith(outer.getNameWithoutPackage() + '\$')) { + return name.substring(outer.getNameWithoutPackage().length() + 1) } if (name.indexOf('$') >= 0 && type.isResolved()) { try { - return type.getTypeClass().getSimpleName(); + return type.getTypeClass().getSimpleName() } catch (RuntimeException | LinkageError ignored) { // not loadable here: keep the binary name rather than guess where the nesting splits } } - return name; + return name } private boolean isValidJavaIdentifier(String name) { - return SourceVersion.isIdentifier(name) && !SourceVersion.isKeyword(name); + return SourceVersion.isIdentifier(name) && !SourceVersion.isKeyword(name) } // A single identifier or a dotted sequence of them, none of which is a keyword. private boolean isValidQualifiedName(String name) { - return SourceVersion.isName(name); + return SourceVersion.isName(name) } private void addError(ASTNode node, SourceUnit source, String message) { source.getErrorCollector().addErrorAndContinue( new org.codehaus.groovy.control.messages.SyntaxErrorMessage( - new SyntaxException(message, node.getLineNumber(), node.getColumnNumber()), source)); + new SyntaxException(message, node.getLineNumber(), node.getColumnNumber()), source)) } } diff --git a/grails-beans-dsl/src/main/java/org/grails/compiler/beans/OnGrailsEnvCondition.java b/grails-beans-dsl/src/main/groovy/org/grails/compiler/beans/OnGrailsEnvCondition.groovy similarity index 71% rename from grails-beans-dsl/src/main/java/org/grails/compiler/beans/OnGrailsEnvCondition.java rename to grails-beans-dsl/src/main/groovy/org/grails/compiler/beans/OnGrailsEnvCondition.groovy index 9e6373e61fc..f0e1481abdf 100644 --- a/grails-beans-dsl/src/main/java/org/grails/compiler/beans/OnGrailsEnvCondition.java +++ b/grails-beans-dsl/src/main/groovy/org/grails/compiler/beans/OnGrailsEnvCondition.groovy @@ -16,16 +16,16 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.compiler.beans; +package org.grails.compiler.beans -import java.lang.reflect.Method; -import java.util.Locale; -import java.util.Map; +import java.lang.reflect.Method -import org.springframework.context.annotation.Condition; -import org.springframework.context.annotation.ConditionContext; -import org.springframework.core.type.AnnotatedTypeMetadata; -import org.springframework.util.ClassUtils; +import groovy.transform.CompileStatic + +import org.springframework.context.annotation.Condition +import org.springframework.context.annotation.ConditionContext +import org.springframework.core.type.AnnotatedTypeMetadata +import org.springframework.util.ClassUtils /** * Matches when the current Grails environment is one of those named by @@ -38,40 +38,41 @@ * was designed for - an explicitly set environment - and leaves the condition unmatched otherwise * rather than guessing.

*/ -public class OnGrailsEnvCondition implements Condition { +@CompileStatic +class OnGrailsEnvCondition implements Condition { - private static final String ENVIRONMENT_CLASS = "grails.util.Environment"; - private static final String ENVIRONMENT_PROPERTY = "grails.env"; - private static final String ANNOTATION_NAME = "grails.compiler.beans.ConditionalOnGrailsEnv"; + private static final String ENVIRONMENT_CLASS = 'grails.util.Environment' + private static final String ENVIRONMENT_PROPERTY = 'grails.env' + private static final String ANNOTATION_NAME = 'grails.compiler.beans.ConditionalOnGrailsEnv' @Override - public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { - Map attributes = metadata.getAnnotationAttributes(ANNOTATION_NAME); + boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + Map attributes = metadata.getAnnotationAttributes(ANNOTATION_NAME) if (attributes == null) { - return false; + return false } - Object value = attributes.get("value"); + Object value = attributes.get('value') if (!(value instanceof String[])) { - return false; + return false } - String current = currentEnvironmentName(context); + String current = currentEnvironmentName(context) if (current == null) { - return false; + return false } - for (String candidate : (String[]) value) { + for (String candidate in (String[]) value) { if (current.equalsIgnoreCase(candidate)) { - return true; + return true } } - return false; + false } private String currentEnvironmentName(ConditionContext context) { - String fromGrails = currentEnvironmentNameFromGrails(context.getClassLoader()); + String fromGrails = currentEnvironmentNameFromGrails(context.getClassLoader()) if (fromGrails != null) { - return fromGrails; + return fromGrails } - return context.getEnvironment().getProperty(ENVIRONMENT_PROPERTY); + context.getEnvironment().getProperty(ENVIRONMENT_PROPERTY) } private String currentEnvironmentNameFromGrails(ClassLoader classLoader) { @@ -80,15 +81,15 @@ private String currentEnvironmentNameFromGrails(ClassLoader classLoader) { // Spring's own class-presence checks fall back to getDefaultClassLoader(), which tries // the thread context loader first - the one that can see Grails when this class and the // application are loaded by different loaders, as under Boot's LaunchedClassLoader. - ClassLoader loader = classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader(); - Class environmentClass = Class.forName(ENVIRONMENT_CLASS, true, loader); - Object current = environmentClass.getMethod("getCurrent").invoke(null); + ClassLoader loader = classLoader != null ? classLoader : ClassUtils.getDefaultClassLoader() + Class environmentClass = Class.forName(ENVIRONMENT_CLASS, true, loader) + Object current = environmentClass.getMethod('getCurrent').invoke(null) if (current == null) { - return null; + return null } - Method getName = environmentClass.getMethod("getName"); - Object name = getName.invoke(current); - return name == null ? null : name.toString().toLowerCase(Locale.ENGLISH); + Method getName = environmentClass.getMethod('getName') + Object name = getName.invoke(current) + return name == null ? null : name.toString().toLowerCase(Locale.ENGLISH) } catch (ReflectiveOperationException | RuntimeException | LinkageError ignored) { // Not a Grails application, or an Environment that cannot answer - fall back to the @@ -102,7 +103,7 @@ private String currentEnvironmentNameFromGrails(ClassLoader classLoader) { // ReflectiveOperationException, and either would otherwise be thrown out of matches() // and fail the whole configuration this condition was meant to skip quietly. // VirtualMachineError is deliberately still not caught. - return null; + return null } } From fa16eae2fbdba9e4b69cafecbdcfd5a7d7fd636a Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Sat, 10 Oct 2026 12:07:25 -0500 Subject: [PATCH 04/14] refactor: convert grails-async-core to Groovy Converts the Java sources of grails-async-core to Groovy. Part of the per-project split requested by the maintainers on #16351; end state taken from the 9.0.x-java-to-groovy branch (origin/9.0.x..e55fd9e0eb). Co-Authored-By: Claude Sonnet 5.5 --- ...syncTransactionalMethodTransformer.groovy} | 14 +- ...ava => DelegateAsyncTransformation.groovy} | 246 +++++++++--------- .../DelegateAsyncTransformationSpec.groovy | 94 +++++++ 3 files changed, 225 insertions(+), 129 deletions(-) rename grails-async/core/src/main/groovy/org/grails/async/transform/internal/{DelegateAsyncTransactionalMethodTransformer.java => DelegateAsyncTransactionalMethodTransformer.groovy} (79%) rename grails-async/core/src/main/groovy/org/grails/async/transform/internal/{DelegateAsyncTransformation.java => DelegateAsyncTransformation.groovy} (57%) create mode 100644 grails-async/core/src/test/groovy/org/grails/async/transform/internal/DelegateAsyncTransformationSpec.groovy diff --git a/grails-async/core/src/main/groovy/org/grails/async/transform/internal/DelegateAsyncTransactionalMethodTransformer.java b/grails-async/core/src/main/groovy/org/grails/async/transform/internal/DelegateAsyncTransactionalMethodTransformer.groovy similarity index 79% rename from grails-async/core/src/main/groovy/org/grails/async/transform/internal/DelegateAsyncTransactionalMethodTransformer.java rename to grails-async/core/src/main/groovy/org/grails/async/transform/internal/DelegateAsyncTransactionalMethodTransformer.groovy index 63c9e104197..3d7bae281f0 100644 --- a/grails-async/core/src/main/groovy/org/grails/async/transform/internal/DelegateAsyncTransactionalMethodTransformer.java +++ b/grails-async/core/src/main/groovy/org/grails/async/transform/internal/DelegateAsyncTransactionalMethodTransformer.groovy @@ -16,11 +16,11 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.async.transform.internal; +package org.grails.async.transform.internal -import org.codehaus.groovy.ast.ClassNode; -import org.codehaus.groovy.ast.MethodNode; -import org.codehaus.groovy.ast.expr.ListExpression; +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.expr.ListExpression /** * Interface for a class that handles transforming async transactional methods @@ -28,6 +28,8 @@ * @author Graeme Rocher * @since 2.3 */ -public interface DelegateAsyncTransactionalMethodTransformer { - void transformTransactionalMethod(ClassNode classNode, ClassNode delegateClassNode, MethodNode methodNode, ListExpression promiseDecoratorLookupArguments); +interface DelegateAsyncTransactionalMethodTransformer { + + void transformTransactionalMethod(ClassNode classNode, ClassNode delegateClassNode, MethodNode methodNode, ListExpression promiseDecoratorLookupArguments) + } diff --git a/grails-async/core/src/main/groovy/org/grails/async/transform/internal/DelegateAsyncTransformation.java b/grails-async/core/src/main/groovy/org/grails/async/transform/internal/DelegateAsyncTransformation.groovy similarity index 57% rename from grails-async/core/src/main/groovy/org/grails/async/transform/internal/DelegateAsyncTransformation.java rename to grails-async/core/src/main/groovy/org/grails/async/transform/internal/DelegateAsyncTransformation.groovy index 35e98f2f184..db61aff39fc 100644 --- a/grails-async/core/src/main/groovy/org/grails/async/transform/internal/DelegateAsyncTransformation.java +++ b/grails-async/core/src/main/groovy/org/grails/async/transform/internal/DelegateAsyncTransformation.groovy @@ -16,46 +16,43 @@ * specific language governing permissions and limitations * under the License. */ -package org.grails.async.transform.internal; - -import java.beans.Introspector; -import java.lang.reflect.Modifier; -import java.util.Arrays; -import java.util.List; - -import groovy.lang.Closure; -import groovy.lang.GroovyObjectSupport; -import org.codehaus.groovy.GroovyBugError; -import org.codehaus.groovy.ast.ASTNode; -import org.codehaus.groovy.ast.AnnotatedNode; -import org.codehaus.groovy.ast.AnnotationNode; -import org.codehaus.groovy.ast.ClassHelper; -import org.codehaus.groovy.ast.ClassNode; -import org.codehaus.groovy.ast.FieldNode; -import org.codehaus.groovy.ast.GenericsType; -import org.codehaus.groovy.ast.MethodNode; -import org.codehaus.groovy.ast.Parameter; -import org.codehaus.groovy.ast.VariableScope; -import org.codehaus.groovy.ast.expr.ArgumentListExpression; -import org.codehaus.groovy.ast.expr.ClassExpression; -import org.codehaus.groovy.ast.expr.ClosureExpression; -import org.codehaus.groovy.ast.expr.ConstructorCallExpression; -import org.codehaus.groovy.ast.expr.Expression; -import org.codehaus.groovy.ast.expr.ListExpression; -import org.codehaus.groovy.ast.expr.MethodCallExpression; -import org.codehaus.groovy.ast.expr.VariableExpression; -import org.codehaus.groovy.ast.stmt.BlockStatement; -import org.codehaus.groovy.ast.stmt.ExpressionStatement; -import org.codehaus.groovy.ast.tools.GenericsUtils; -import org.codehaus.groovy.control.SourceUnit; -import org.codehaus.groovy.transform.ASTTransformation; -import org.codehaus.groovy.transform.GroovyASTTransformation; -import org.codehaus.groovy.transform.TransformWithPriority; -import org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport; - -import grails.async.Promise; -import grails.async.Promises; -import org.apache.grails.common.compiler.GroovyTransformOrder; +package org.grails.async.transform.internal + +import java.beans.Introspector +import java.lang.reflect.Modifier + +import groovy.transform.CompileStatic +import org.codehaus.groovy.GroovyBugError +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.AnnotatedNode +import org.codehaus.groovy.ast.AnnotationNode +import org.codehaus.groovy.ast.ClassHelper +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.FieldNode +import org.codehaus.groovy.ast.GenericsType +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.Parameter +import org.codehaus.groovy.ast.VariableScope +import org.codehaus.groovy.ast.expr.ArgumentListExpression +import org.codehaus.groovy.ast.expr.ClassExpression +import org.codehaus.groovy.ast.expr.ClosureExpression +import org.codehaus.groovy.ast.expr.ConstructorCallExpression +import org.codehaus.groovy.ast.expr.Expression +import org.codehaus.groovy.ast.expr.ListExpression +import org.codehaus.groovy.ast.expr.MethodCallExpression +import org.codehaus.groovy.ast.expr.VariableExpression +import org.codehaus.groovy.ast.stmt.BlockStatement +import org.codehaus.groovy.ast.stmt.ExpressionStatement +import org.codehaus.groovy.ast.tools.GenericsUtils +import org.codehaus.groovy.control.SourceUnit +import org.codehaus.groovy.transform.ASTTransformation +import org.codehaus.groovy.transform.GroovyASTTransformation +import org.codehaus.groovy.transform.TransformWithPriority +import org.codehaus.groovy.transform.stc.StaticTypeCheckingSupport + +import grails.async.Promise +import grails.async.Promises +import org.apache.grails.common.compiler.GroovyTransformOrder /** * Implementation of {@link grails.async.DelegateAsync} transformation @@ -64,107 +61,109 @@ * @since 2.3 */ @GroovyASTTransformation -public class DelegateAsyncTransformation implements ASTTransformation, TransformWithPriority { - private static final ArgumentListExpression NO_ARGS = new ArgumentListExpression(); - private static final String VOID = "void"; - public static final ClassNode GROOVY_OBJECT_CLASS_NODE = new ClassNode(GroovyObjectSupport.class); - public static final ClassNode OBJECT_CLASS_NODE = new ClassNode(Object.class); +@CompileStatic +class DelegateAsyncTransformation implements ASTTransformation, TransformWithPriority { + + private static final ArgumentListExpression NO_ARGS = new ArgumentListExpression() + private static final String VOID = 'void' + public static final ClassNode GROOVY_OBJECT_CLASS_NODE = new ClassNode(GroovyObjectSupport) + public static final ClassNode OBJECT_CLASS_NODE = new ClassNode(Object) @Override - public void visit(ASTNode[] nodes, SourceUnit source) { + void visit(ASTNode[] nodes, SourceUnit source) { if (nodes.length != 2 || !(nodes[0] instanceof AnnotationNode) || !(nodes[1] instanceof AnnotatedNode)) { - throw new GroovyBugError("Internal error: expecting [AnnotationNode, AnnotatedNode] but got: " + Arrays.asList(nodes)); + throw new GroovyBugError('Internal error: expecting [AnnotationNode, AnnotatedNode] but got: ' + Arrays.asList(nodes)) } - AnnotatedNode parent = (AnnotatedNode) nodes[1]; - AnnotationNode annotationNode = (AnnotationNode) nodes[0]; + AnnotatedNode parent = (AnnotatedNode) nodes[1] + AnnotationNode annotationNode = (AnnotationNode) nodes[0] if (parent instanceof ClassNode) { - Expression value = annotationNode.getMember("value"); + Expression value = annotationNode.getMember('value') if (value instanceof ClassExpression) { - ClassNode targetApi = value.getType().getPlainNodeReference(); - ClassNode classNode = (ClassNode) parent; + ClassNode targetApi = value.getType().getPlainNodeReference() + ClassNode classNode = (ClassNode) parent - final String fieldName = '$' + Introspector.decapitalize(targetApi.getNameWithoutPackage()); - FieldNode fieldNode = classNode.getField(fieldName); + final String fieldName = '$' + Introspector.decapitalize(targetApi.getNameWithoutPackage()) + FieldNode fieldNode = classNode.getField(fieldName) if (fieldNode == null) { - fieldNode = new FieldNode(fieldName, Modifier.PRIVATE, targetApi, classNode, new ConstructorCallExpression(targetApi, NO_ARGS)); - classNode.addField(fieldNode); + fieldNode = new FieldNode(fieldName, Modifier.PRIVATE, targetApi, classNode, new ConstructorCallExpression(targetApi, NO_ARGS)) + classNode.addField(fieldNode) } - applyDelegateAsyncTransform(classNode, targetApi, fieldName); + applyDelegateAsyncTransform(classNode, targetApi, fieldName) } } else if (parent instanceof FieldNode) { - FieldNode fieldNode = (FieldNode) parent; - ClassNode targetApi = fieldNode.getType().getPlainNodeReference(); - ClassNode classNode = fieldNode.getOwner(); - applyDelegateAsyncTransform(classNode, targetApi, fieldNode.getName()); + FieldNode fieldNode = (FieldNode) parent + ClassNode targetApi = fieldNode.getType().getPlainNodeReference() + ClassNode classNode = fieldNode.getOwner() + applyDelegateAsyncTransform(classNode, targetApi, fieldNode.getName()) } } private void applyDelegateAsyncTransform(ClassNode classNode, ClassNode targetApi, String fieldName) { - List methods = targetApi.getAllDeclaredMethods(); + List methods = targetApi.getAllDeclaredMethods() - ClassNode promisesClass = ClassHelper.make(Promises.class).getPlainNodeReference(); - MethodNode createPromiseMethodTargetWithDecorators = promisesClass.getDeclaredMethod("createPromise", new Parameter[]{new Parameter(new ClassNode(Closure.class), "c"), new Parameter(new ClassNode(List.class), "c")}); + ClassNode promisesClass = ClassHelper.make(Promises).getPlainNodeReference() + MethodNode createPromiseMethodTargetWithDecorators = promisesClass.getDeclaredMethod('createPromise', [new Parameter(new ClassNode(Closure), 'c'), new Parameter(new ClassNode(List), 'c')] as Parameter[]) - DelegateAsyncTransactionalMethodTransformer delegateAsyncTransactionalMethodTransformer = lookupAsyncTransactionalMethodTransformer(); - for (MethodNode m : methods) { + DelegateAsyncTransactionalMethodTransformer delegateAsyncTransactionalMethodTransformer = lookupAsyncTransactionalMethodTransformer() + for (MethodNode m in methods) { if (isCandidateMethod(m)) { - MethodNode existingMethod = classNode.getMethod(m.getName(), m.getParameters()); + MethodNode existingMethod = classNode.getMethod(m.getName(), m.getParameters()) if (existingMethod == null) { - ClassNode promiseNode = ClassHelper.make(Promise.class).getPlainNodeReference(); - ClassNode originalReturnType = m.getReturnType(); + ClassNode promiseNode = ClassHelper.make(Promise).getPlainNodeReference() + ClassNode originalReturnType = m.getReturnType() if (!VOID.equals(originalReturnType.getNameWithoutPackage())) { - ClassNode returnType; + ClassNode returnType if (ClassHelper.isPrimitiveType(originalReturnType.redirect())) { - returnType = ClassHelper.getWrapper(originalReturnType.redirect()); + returnType = ClassHelper.getWrapper(originalReturnType.redirect()) } else { - returnType = alignReturnType(classNode, originalReturnType); + returnType = alignReturnType(classNode, originalReturnType) } if (!OBJECT_CLASS_NODE.equals(returnType)) { - promiseNode.setGenericsTypes(new GenericsType[]{new GenericsType(returnType)}); + promiseNode.setGenericsTypes([new GenericsType(returnType)] as GenericsType[]) } } - final BlockStatement methodBody = new BlockStatement(); - final BlockStatement promiseBody = new BlockStatement(); + final BlockStatement methodBody = new BlockStatement() + final BlockStatement promiseBody = new BlockStatement() - final ClosureExpression closureExpression = new ClosureExpression(new Parameter[0], promiseBody); - VariableScope variableScope = new VariableScope(); - closureExpression.setVariableScope(variableScope); - VariableExpression thisObject = new VariableExpression("this"); - ClassNode delegateAsyncUtilsClassNode = new ClassNode(DelegateAsyncUtils.class); + final ClosureExpression closureExpression = new ClosureExpression(new Parameter[0], promiseBody) + VariableScope variableScope = new VariableScope() + closureExpression.setVariableScope(variableScope) + VariableExpression thisObject = new VariableExpression('this') + ClassNode delegateAsyncUtilsClassNode = new ClassNode(DelegateAsyncUtils) - MethodNode getPromiseDecoratorsMethodNode = delegateAsyncUtilsClassNode.getDeclaredMethods("getPromiseDecorators").get(0); - ListExpression promiseDecorators = new ListExpression(); - ArgumentListExpression getPromiseDecoratorsArguments = new ArgumentListExpression(thisObject, promiseDecorators); - delegateAsyncTransactionalMethodTransformer.transformTransactionalMethod(classNode, targetApi, m, promiseDecorators); + MethodNode getPromiseDecoratorsMethodNode = delegateAsyncUtilsClassNode.getDeclaredMethods('getPromiseDecorators').get(0) + ListExpression promiseDecorators = new ListExpression() + ArgumentListExpression getPromiseDecoratorsArguments = new ArgumentListExpression(thisObject, promiseDecorators) + delegateAsyncTransactionalMethodTransformer.transformTransactionalMethod(classNode, targetApi, m, promiseDecorators) - MethodCallExpression getDecoratorsMethodCall = new MethodCallExpression(new ClassExpression(delegateAsyncUtilsClassNode), "getPromiseDecorators", getPromiseDecoratorsArguments); - getDecoratorsMethodCall.setMethodTarget(getPromiseDecoratorsMethodNode); + MethodCallExpression getDecoratorsMethodCall = new MethodCallExpression(new ClassExpression(delegateAsyncUtilsClassNode), 'getPromiseDecorators', getPromiseDecoratorsArguments) + getDecoratorsMethodCall.setMethodTarget(getPromiseDecoratorsMethodNode) - MethodCallExpression createPromiseWithDecorators = new MethodCallExpression(new ClassExpression(promisesClass), "createPromise", new ArgumentListExpression(closureExpression, getDecoratorsMethodCall)); + MethodCallExpression createPromiseWithDecorators = new MethodCallExpression(new ClassExpression(promisesClass), 'createPromise', new ArgumentListExpression(closureExpression, getDecoratorsMethodCall)) if (createPromiseMethodTargetWithDecorators != null) { - createPromiseWithDecorators.setMethodTarget(createPromiseMethodTargetWithDecorators); + createPromiseWithDecorators.setMethodTarget(createPromiseMethodTargetWithDecorators) } - methodBody.addStatement(new ExpressionStatement(createPromiseWithDecorators)); + methodBody.addStatement(new ExpressionStatement(createPromiseWithDecorators)) - final ArgumentListExpression arguments = new ArgumentListExpression(); + final ArgumentListExpression arguments = new ArgumentListExpression() - Parameter[] parameters = copyParameters(StaticTypeCheckingSupport.parameterizeArguments(classNode, m)); - for (Parameter p : parameters) { - p.setClosureSharedVariable(true); - variableScope.putReferencedLocalVariable(p); - VariableExpression ve = new VariableExpression(p); - ve.setClosureSharedVariable(true); - arguments.addExpression(ve); + Parameter[] parameters = copyParameters(StaticTypeCheckingSupport.parameterizeArguments(classNode, m)) + for (Parameter p in parameters) { + p.setClosureSharedVariable(true) + variableScope.putReferencedLocalVariable(p) + VariableExpression ve = new VariableExpression(p) + ve.setClosureSharedVariable(true) + arguments.addExpression(ve) } - MethodCallExpression delegateMethodCall = new MethodCallExpression(new VariableExpression(fieldName), m.getName(), arguments); - promiseBody.addStatement(new ExpressionStatement(delegateMethodCall)); - MethodNode newMethodNode = new MethodNode(m.getName(), Modifier.PUBLIC, promiseNode, parameters, ClassNode.EMPTY_ARRAY, methodBody); - classNode.addMethod(newMethodNode); + MethodCallExpression delegateMethodCall = new MethodCallExpression(new VariableExpression(fieldName), m.getName(), arguments) + promiseBody.addStatement(new ExpressionStatement(delegateMethodCall)) + MethodNode newMethodNode = new MethodNode(m.getName(), Modifier.PUBLIC, promiseNode, parameters, ClassNode.EMPTY_ARRAY, methodBody) + classNode.addMethod(newMethodNode) } } } @@ -172,58 +171,59 @@ private void applyDelegateAsyncTransform(ClassNode classNode, ClassNode targetAp private static ClassNode alignReturnType(final ClassNode receiver, final ClassNode originalReturnType) { if (!originalReturnType.isUsingGenerics()) { - return originalReturnType.getPlainNodeReference(); + return originalReturnType.getPlainNodeReference() } - var genericsSpec = GenericsUtils.createGenericsSpec(receiver); - var correctedReturnType = GenericsUtils.correctToGenericsSpecRecurse(genericsSpec, originalReturnType); + Map genericsSpec = GenericsUtils.createGenericsSpec(receiver) + ClassNode correctedReturnType = GenericsUtils.correctToGenericsSpecRecurse(genericsSpec, originalReturnType) return correctedReturnType == null ? originalReturnType.getPlainNodeReference() : - correctedReturnType; + correctedReturnType } protected DelegateAsyncTransactionalMethodTransformer lookupAsyncTransactionalMethodTransformer() { try { - Class transformerClass = getClass().getClassLoader().loadClass("org.grails.async.transform.internal.DefaultDelegateAsyncTransactionalMethodTransformer"); - return (DelegateAsyncTransactionalMethodTransformer) transformerClass.getDeclaredConstructor().newInstance(); + Class transformerClass = getClass().getClassLoader().loadClass('org.grails.async.transform.internal.DefaultDelegateAsyncTransactionalMethodTransformer') + return (DelegateAsyncTransactionalMethodTransformer) transformerClass.getDeclaredConstructor().newInstance() } catch (Exception ignored) { // ignore } - return new NoopDelegateAsyncTransactionalMethodTransformer(); + return new NoopDelegateAsyncTransactionalMethodTransformer() } private static boolean isCandidateMethod(MethodNode declaredMethod) { - String methodName = declaredMethod.getName(); + String methodName = declaredMethod.getName() return !declaredMethod.isSynthetic() && - !methodName.contains("$") && + !methodName.contains('\$') && Modifier.isPublic(declaredMethod.getModifiers()) && - !GROOVY_OBJECT_CLASS_NODE.hasMethod(declaredMethod.getName(), declaredMethod.getParameters()); + !GROOVY_OBJECT_CLASS_NODE.hasMethod(declaredMethod.getName(), declaredMethod.getParameters()) } private static Parameter[] copyParameters(Parameter... parameterTypes) { - Parameter[] newParameterTypes = new Parameter[parameterTypes.length]; + Parameter[] newParameterTypes = new Parameter[parameterTypes.length] for (int i = 0; i < parameterTypes.length; i++) { - Parameter parameterType = parameterTypes[i]; - ClassNode parameterTypeCN = parameterType.getType(); - ClassNode newParameterTypeCN = parameterTypeCN.getPlainNodeReference(); + Parameter parameterType = parameterTypes[i] + ClassNode parameterTypeCN = parameterType.getType() + ClassNode newParameterTypeCN = parameterTypeCN.getPlainNodeReference() if (parameterTypeCN.isUsingGenerics() && !parameterTypeCN.isGenericsPlaceHolder()) { - newParameterTypeCN.setGenericsTypes(parameterTypeCN.getGenericsTypes()); + newParameterTypeCN.setGenericsTypes(parameterTypeCN.getGenericsTypes()) } - Parameter newParameter = new Parameter(newParameterTypeCN, parameterType.getName(), parameterType.getInitialExpression()); - newParameter.addAnnotations(parameterType.getAnnotations()); - newParameterTypes[i] = newParameter; + Parameter newParameter = new Parameter(newParameterTypeCN, parameterType.getName(), parameterType.getInitialExpression()) + newParameter.addAnnotations(parameterType.getAnnotations()) + newParameterTypes[i] = newParameter } - return newParameterTypes; + return newParameterTypes } @Override - public int priority() { - return GroovyTransformOrder.DELEGATE_ASYNC_ORDER; + int priority() { + return GroovyTransformOrder.DELEGATE_ASYNC_ORDER } private static class NoopDelegateAsyncTransactionalMethodTransformer implements DelegateAsyncTransactionalMethodTransformer { @Override - public void transformTransactionalMethod(ClassNode classNode, ClassNode delegateClassNode, MethodNode methodNode, ListExpression promiseDecoratorLookupArguments) { + void transformTransactionalMethod(ClassNode classNode, ClassNode delegateClassNode, MethodNode methodNode, ListExpression promiseDecoratorLookupArguments) { // noop } } + } diff --git a/grails-async/core/src/test/groovy/org/grails/async/transform/internal/DelegateAsyncTransformationSpec.groovy b/grails-async/core/src/test/groovy/org/grails/async/transform/internal/DelegateAsyncTransformationSpec.groovy new file mode 100644 index 00000000000..afe93536b79 --- /dev/null +++ b/grails-async/core/src/test/groovy/org/grails/async/transform/internal/DelegateAsyncTransformationSpec.groovy @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package org.grails.async.transform.internal + +import java.lang.reflect.Method +import java.lang.reflect.ParameterizedType + +import org.codehaus.groovy.GroovyBugError +import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.expr.ListExpression +import spock.lang.Specification + +import grails.async.Promise +import org.apache.grails.common.compiler.GroovyTransformOrder + +class DelegateAsyncTransformationSpec extends Specification { + + void 'the transformation validates its input nodes and declares its priority'() { + given: + DelegateAsyncTransformation transformation = new DelegateAsyncTransformation() + + when: + transformation.visit([new ClassNode(String)] as ASTNode[], null) + + then: + GroovyBugError e = thrown() + e.message.contains('Internal error: expecting [AnnotationNode, AnnotatedNode] but got: [java.lang.String]') + transformation.priority() == GroovyTransformOrder.DELEGATE_ASYNC_ORDER + DelegateAsyncTransformation.OBJECT_CLASS_NODE.name == 'java.lang.Object' + DelegateAsyncTransformation.GROOVY_OBJECT_CLASS_NODE.name == 'groovy.lang.GroovyObjectSupport' + } + + void 'the transactional method transformer contract is a single callback'() { + expect: + DelegateAsyncTransactionalMethodTransformer.isInterface() + DelegateAsyncTransactionalMethodTransformer.getMethod('transformTransactionalMethod', ClassNode, ClassNode, MethodNode, ListExpression).returnType == void + } + + void 'a class level annotation creates the delegate field and wraps every public method'() { + given: + GroovyClassLoader gcl = new GroovyClassLoader() + gcl.parseClass(''' + import grails.async.DelegateAsync + + class CalcApi { + int add(int a, int b) { a + b } + Object anything() { 'x' } + void fire() { } + static int ignored() { 1 } + } + @DelegateAsync(CalcApi) + class AsyncCalc { + String anything() { 'declared' } + } + ''') + Class cls = gcl.loadClass('AsyncCalc') + Object instance = cls.getDeclaredConstructor().newInstance() + + when: + Method add = cls.getMethod('add', int, int) + Method fire = cls.getMethod('fire') + + then: + cls.getDeclaredField('$calcApi').type.name == 'CalcApi' + add.returnType == Promise + ((ParameterizedType) add.genericReturnType).actualTypeArguments == [Integer] as Class[] + fire.returnType == Promise + !(fire.genericReturnType instanceof ParameterizedType) + cls.getMethod('anything').returnType == String + cls.getMethod('ignored').returnType == Promise + !java.lang.reflect.Modifier.isStatic(cls.getMethod('ignored').modifiers) + ((Promise) instance.add(2, 3)).get() == 5 + ((Promise) instance.fire()).get() == null + } + +} From 630b069ff22c1623bd69afd8a82ae77a2639d8b9 Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Sun, 11 Oct 2026 14:47:39 -0500 Subject: [PATCH 05/14] test: cover the converted core-leaf classes Co-Authored-By: Claude Sonnet 5.5 --- ...AsyncTransactionalMethodTransformer.groovy | 41 ++++ .../DelegateAsyncTransformationSpec.groovy | 229 ++++++++++++++++++ .../AutoConfigurationImportsWriterSpec.groovy | 172 +++++++++++++ .../beans/OnGrailsEnvConditionSpec.groovy | 80 ++++++ .../common/aot/RegistrableTypesSpec.groovy | 132 ++++++++++ .../common/reflect/ReflectionUtilsSpec.groovy | 81 +++++++ 6 files changed, 735 insertions(+) create mode 100644 grails-async/core/src/test/groovy/org/grails/async/transform/internal/DefaultDelegateAsyncTransactionalMethodTransformer.groovy diff --git a/grails-async/core/src/test/groovy/org/grails/async/transform/internal/DefaultDelegateAsyncTransactionalMethodTransformer.groovy b/grails-async/core/src/test/groovy/org/grails/async/transform/internal/DefaultDelegateAsyncTransactionalMethodTransformer.groovy new file mode 100644 index 00000000000..15587a162f4 --- /dev/null +++ b/grails-async/core/src/test/groovy/org/grails/async/transform/internal/DefaultDelegateAsyncTransactionalMethodTransformer.groovy @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package org.grails.async.transform.internal + +import org.codehaus.groovy.ast.ClassNode +import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.expr.ListExpression + +/** + * Stands in for the transactional transformer the async plugin supplies. {@code DelegateAsyncTransformation} + * looks the implementation up by this name, so having it on the test classpath is what lets the + * lookup succeed in this module, and recording the calls shows that the transformation consults it + * once for every method it wraps. + */ +class DefaultDelegateAsyncTransactionalMethodTransformer implements DelegateAsyncTransactionalMethodTransformer { + + static final List TRANSFORMED = Collections.synchronizedList([]) + + @Override + void transformTransactionalMethod(ClassNode classNode, ClassNode delegateClassNode, MethodNode methodNode, + ListExpression promiseDecoratorLookupArguments) { + TRANSFORMED.add(delegateClassNode.name + '.' + methodNode.name) + } + +} diff --git a/grails-async/core/src/test/groovy/org/grails/async/transform/internal/DelegateAsyncTransformationSpec.groovy b/grails-async/core/src/test/groovy/org/grails/async/transform/internal/DelegateAsyncTransformationSpec.groovy index afe93536b79..7f1cd9fa41d 100644 --- a/grails-async/core/src/test/groovy/org/grails/async/transform/internal/DelegateAsyncTransformationSpec.groovy +++ b/grails-async/core/src/test/groovy/org/grails/async/transform/internal/DelegateAsyncTransformationSpec.groovy @@ -23,11 +23,16 @@ import java.lang.reflect.ParameterizedType import org.codehaus.groovy.GroovyBugError import org.codehaus.groovy.ast.ASTNode +import org.codehaus.groovy.ast.AnnotationNode +import org.codehaus.groovy.ast.ClassHelper import org.codehaus.groovy.ast.ClassNode import org.codehaus.groovy.ast.MethodNode +import org.codehaus.groovy.ast.Parameter import org.codehaus.groovy.ast.expr.ListExpression +import org.codehaus.groovy.ast.stmt.BlockStatement import spock.lang.Specification +import grails.async.DelegateAsync import grails.async.Promise import org.apache.grails.common.compiler.GroovyTransformOrder @@ -91,4 +96,228 @@ class DelegateAsyncTransformationSpec extends Specification { ((Promise) instance.fire()).get() == null } + void 'input that is not an annotation on an annotated node is rejected'() { + given: + DelegateAsyncTransformation transformation = new DelegateAsyncTransformation() + + when: + transformation.visit(nodes as ASTNode[], null) + + then: + thrown(GroovyBugError) + + where: + nodes << [ + [new AnnotationNode(ClassHelper.make(DelegateAsync))], + [new AnnotationNode(ClassHelper.make(DelegateAsync)), new BlockStatement()], + [new AnnotationNode(ClassHelper.make(DelegateAsync)), new ClassNode(String), new ClassNode(String)] + ] + } + + void 'a class level annotation without a delegate type adds nothing'() { + given: + GroovyClassLoader gcl = new GroovyClassLoader() + gcl.parseClass(''' + import grails.async.DelegateAsync + + @DelegateAsync + class Untouched { + String name() { 'plain' } + } + ''') + Class cls = gcl.loadClass('Untouched') + + expect: + cls.declaredFields.findAll { it.name.startsWith('$') && !it.synthetic } == [] + cls.declaredMethods.every { it.returnType != Promise } + } + + void 'a delegate field the class already declares is reused rather than replaced'() { + given: + GroovyClassLoader gcl = new GroovyClassLoader() + gcl.parseClass(''' + import grails.async.DelegateAsync + + class GreeterApi { + String greet(String who) { "hello ${who}" } + } + @DelegateAsync(GreeterApi) + class AsyncGreeter { + private GreeterApi $greeterApi = new GreeterApi() { + String greet(String who) { "custom ${who}" } + } + } + ''') + Class cls = gcl.loadClass('AsyncGreeter') + Object instance = cls.getDeclaredConstructor().newInstance() + + expect: + cls.declaredFields.findAll { it.name == '$greeterApi' }.size() == 1 + ((Promise) instance.greet('you')).get() == 'custom you' + } + + void 'an annotated field becomes the delegate of its owning class'() { + given: + GroovyClassLoader gcl = new GroovyClassLoader() + gcl.parseClass(''' + import grails.async.DelegateAsync + + class CounterApi { + int count(String text) { text.length() } + } + class AsyncCounter { + @DelegateAsync + private CounterApi counter = new CounterApi() + } + ''') + Class cls = gcl.loadClass('AsyncCounter') + Object instance = cls.getDeclaredConstructor().newInstance() + + expect: + cls.getMethod('count', String).returnType == Promise + ((Promise) instance.count('four')).get() == 4 + } + + void 'only public methods the class does not declare are wrapped'() { + given: + GroovyClassLoader gcl = new GroovyClassLoader() + gcl.parseClass(''' + import grails.async.DelegateAsync + + class MixedApi { + String visible() { 'visible' } + protected String guarded() { 'guarded' } + private String hidden() { 'hidden' } + } + @DelegateAsync(MixedApi) + class AsyncMixed { + } + ''') + Class cls = gcl.loadClass('AsyncMixed') + Set wrapped = cls.declaredMethods.findAll { it.returnType == Promise }*.name as Set + + expect: + wrapped == ['visible'] as Set + } + + void 'generic return and parameter types are carried over to the wrapper'() { + given: + GroovyClassLoader gcl = new GroovyClassLoader() + gcl.parseClass(''' + import grails.async.DelegateAsync + + class RepoApi { + List names() { ['a', 'b'] } + int total(List numbers) { numbers.sum() as int } + } + @DelegateAsync(RepoApi) + class AsyncRepo { + } + ''') + Class cls = gcl.loadClass('AsyncRepo') + Object instance = cls.getDeclaredConstructor().newInstance() + Method names = cls.getMethod('names') + Method total = cls.getMethod('total', List) + + expect: + ((ParameterizedType) names.genericReturnType).actualTypeArguments[0].typeName == 'java.util.List' + ((ParameterizedType) total.genericParameterTypes[0]).actualTypeArguments == [Integer] as Class[] + ((Promise) instance.names()).get() == ['a', 'b'] + ((Promise) instance.total([1, 2, 3])).get() == 6 + } + + void 'a generic delegate type resolves its type variables against the annotated class'() { + given: + GroovyClassLoader gcl = new GroovyClassLoader() + gcl.parseClass(''' + import grails.async.DelegateAsync + + class BoxApi { + T open() { null } + } + @DelegateAsync(BoxApi) + class AsyncBox { + } + ''') + Class cls = gcl.loadClass('AsyncBox') + + expect: + cls.getMethod('open').returnType == Promise + } + + void 'the transactional transformer is consulted once for every wrapped method'() { + given: + DefaultDelegateAsyncTransactionalMethodTransformer.TRANSFORMED.clear() + GroovyClassLoader gcl = new GroovyClassLoader() + + when: + gcl.parseClass(''' + import grails.async.DelegateAsync + + class AuditApi { + void first() { } + int second() { 2 } + } + @DelegateAsync(AuditApi) + class AsyncAudit { + } + ''') + + then: + DefaultDelegateAsyncTransactionalMethodTransformer.TRANSFORMED.containsAll(['AuditApi.first', 'AuditApi.second']) + !DefaultDelegateAsyncTransactionalMethodTransformer.TRANSFORMED.contains('AuditApi.hashCode') + } + + void 'without the plugin transformer on the classpath the methods are wrapped with no transactional handling'() { + given: 'a loader that defines the transformation itself and cannot see the plugin transformer' + String pluginTransformer = 'org.grails.async.transform.internal.DefaultDelegateAsyncTransactionalMethodTransformer' + URL mainClasses = DelegateAsyncTransformation.protectionDomain.codeSource.location + ClassLoader isolated = new URLClassLoader([mainClasses] as URL[], getClass().classLoader) { + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + if (name == pluginTransformer) { + throw new ClassNotFoundException(name) + } + if (name.startsWith('org.grails.async.transform.internal.')) { + synchronized (getClassLoadingLock(name)) { + Class loaded = findLoadedClass(name) ?: findClass(name) + return loaded + } + } + return super.loadClass(name, resolve) + } + } + DefaultDelegateAsyncTransactionalMethodTransformer.TRANSFORMED.clear() + + when: + GroovyClassLoader gcl = new GroovyClassLoader(isolated) + gcl.parseClass(''' + import grails.async.DelegateAsync + + class PlainApi { + int answer() { 42 } + } + @DelegateAsync(PlainApi) + class AsyncPlain { + } + ''') + Class cls = gcl.loadClass('AsyncPlain') + + then: + cls.getMethod('answer').returnType.name == 'grails.async.Promise' + DefaultDelegateAsyncTransactionalMethodTransformer.TRANSFORMED.isEmpty() + } + + void 'an annotation on a node that is neither a class nor a field is ignored'() { + given: + DelegateAsyncTransformation transformation = new DelegateAsyncTransformation() + MethodNode method = new MethodNode('run', 1, ClassHelper.VOID_TYPE, [] as Parameter[], ClassNode.EMPTY_ARRAY, null) + + when: + transformation.visit([new AnnotationNode(ClassHelper.make(DelegateAsync)), method] as ASTNode[], null) + + then: + noExceptionThrown() + } + } diff --git a/grails-beans-dsl/src/test/groovy/org/grails/compiler/beans/AutoConfigurationImportsWriterSpec.groovy b/grails-beans-dsl/src/test/groovy/org/grails/compiler/beans/AutoConfigurationImportsWriterSpec.groovy index 30bec465a2e..480aa083cf1 100644 --- a/grails-beans-dsl/src/test/groovy/org/grails/compiler/beans/AutoConfigurationImportsWriterSpec.groovy +++ b/grails-beans-dsl/src/test/groovy/org/grails/compiler/beans/AutoConfigurationImportsWriterSpec.groovy @@ -278,6 +278,178 @@ class AutoConfigurationImportsWriterSpec extends Specification { source.errorCollector.getSyntaxError(0).message.contains('Could not write generated auto-configuration imports') } + void 'the utility class cannot be instantiated by accident'() { + given: + def constructor = AutoConfigurationImportsWriter.getDeclaredConstructor() + + expect: + java.lang.reflect.Modifier.isPrivate(constructor.modifiers) + + when: + constructor.accessible = true + constructor.newInstance() + + then: + noExceptionThrown() + } + + void 'nothing is written without a class name or an output directory'() { + expect: + !AutoConfigurationImportsWriter.register(className, hasDirectory ? targetDir : null, null, null) + + and: + !new File(targetDir, IMPORTS).exists() + + where: + className | hasDirectory + null | true + '' | true + 'com.example.GreetingConfig' | false + } + + void 'reconciling without an output directory does nothing'() { + expect: + !AutoConfigurationImportsWriter.reconcile(null, null, null) + } + + void 'registering the same class twice writes once'() { + expect: 'the second call finds the file already says what it would write' + AutoConfigurationImportsWriter.register('com.example.GreetingAutoConfiguration', targetDir, null, 'build one') + !AutoConfigurationImportsWriter.register('com.example.GreetingAutoConfiguration', targetDir, null, 'build one') + importsEntries() == ['com.example.GreetingAutoConfiguration'] + } + + void 'a registration with no compilation to scope to is pruned only by the class files present'() { + given: + AutoConfigurationImportsWriter.register('com.example.FirstAutoConfiguration', targetDir, null, null) + + when: 'the second registers with nothing remembering the first, whose class file was never written' + AutoConfigurationImportsWriter.register('com.example.SecondAutoConfiguration', targetDir, null, null) + + then: + importsEntries() == ['com.example.SecondAutoConfiguration'] + } + + void 'reconciling keeps an entry registered by the compilation before its class file is written'() { + given: + Object compilation = new Object() + AutoConfigurationImportsWriter.register('com.example.PendingAutoConfiguration', targetDir, null, compilation) + + expect: + !AutoConfigurationImportsWriter.reconcile(targetDir, compilation, null) + importsEntries() == ['com.example.PendingAutoConfiguration'] + } + + void 'reconciling drops an entry registered by a different compilation whose class is gone'() { + given: + AutoConfigurationImportsWriter.register('com.example.GoneAutoConfiguration', targetDir, null, new Object()) + + expect: + AutoConfigurationImportsWriter.reconcile(targetDir, new Object(), null) + importsEntries() == [] + } + + void 'reconciling removes a generated file left beside a hand-authored one'() { + given: + AutoConfigurationImportsWriter.register('com.example.GreetingAutoConfiguration', targetDir, null, null) + File handAuthored = new File(projectDir, "src/main/resources/${IMPORTS}") + handAuthored.parentFile.mkdirs() + handAuthored.text = 'com.example.GreetingAutoConfiguration\n' + + expect: + AutoConfigurationImportsWriter.reconcile(targetDir, null, null) + !new File(targetDir, IMPORTS).exists() + } + + void 'a hand-authored file missing the generated class is tolerated when there is no source to warn on'() { + given: + File handAuthored = new File(projectDir, "src/main/resources/${IMPORTS}") + handAuthored.parentFile.mkdirs() + handAuthored.text = 'com.elsewhere.FromAnotherJar\n' + + expect: + !AutoConfigurationImportsWriter.register('com.example.GreetingAutoConfiguration', targetDir, null, null) + !new File(targetDir, IMPORTS).exists() + } + + void 'a write failure with no source to report to is an exception'() { + given: + new File(targetDir, 'META-INF').text = 'not a directory' + + when: + AutoConfigurationImportsWriter.register('com.example.BrokenAutoConfiguration', targetDir, null, null) + + then: + IllegalStateException e = thrown() + e.message.contains('Could not write generated auto-configuration imports') + e.cause instanceof IOException + } + + void 'comments and blank lines in an existing file are not carried over as entries'() { + given: + File imports = new File(targetDir, IMPORTS) + imports.parentFile.mkdirs() + imports.text = '# generated\n\n \ncom.example.GreetingAutoConfiguration\n' + new File(targetDir, 'com/example').mkdirs() + new File(targetDir, 'com/example/GreetingAutoConfiguration.class').text = 'class' + + when: + AutoConfigurationImportsWriter.register('com.example.FarewellAutoConfiguration', targetDir, null, null) + + then: + imports.readLines() == ['com.example.FarewellAutoConfiguration', 'com.example.GreetingAutoConfiguration'] + } + + void 'an imports file that cannot be read is replaced by the entry being registered'() { + given: 'bytes that are not valid UTF-8, which the reader rejects' + File imports = new File(targetDir, IMPORTS) + imports.parentFile.mkdirs() + imports.bytes = [0xC3, 0x28, 0xFF] as byte[] + + when: + AutoConfigurationImportsWriter.register('com.example.GreetingAutoConfiguration', targetDir, null, null) + + then: + imports.readLines() == ['com.example.GreetingAutoConfiguration'] + } + + void 'a base directory that does not exist falls back to the directory above the build output'() { + given: + System.setProperty('base.dir', new File(projectDir, 'missing').absolutePath) + File handAuthored = new File(projectDir, "src/main/resources/${IMPORTS}") + handAuthored.parentFile.mkdirs() + handAuthored.text = 'com.elsewhere.FromAnotherJar\n' + + expect: 'the hand-authored file is still found, so nothing is generated beside it' + !AutoConfigurationImportsWriter.register('com.example.GreetingAutoConfiguration', targetDir, null, null) + !new File(targetDir, IMPORTS).exists() + } + + void 'a maven style target directory marks the module root as well'() { + given: + System.clearProperty('base.dir') + File mavenTarget = new File(projectDir, 'target/classes') + mavenTarget.mkdirs() + File handAuthored = new File(projectDir, "src/main/resources/${IMPORTS}") + handAuthored.parentFile.mkdirs() + handAuthored.text = 'com.elsewhere.FromAnotherJar\n' + + expect: + !AutoConfigurationImportsWriter.register('com.example.GreetingAutoConfiguration', mavenTarget, null, null) + !new File(mavenTarget, IMPORTS).exists() + } + + void 'an output directory outside any build or target directory has no hand-authored file to find'() { + given: + System.clearProperty('base.dir') + File elsewhere = new File(projectDir, 'out/classes') + elsewhere.mkdirs() + + expect: + AutoConfigurationImportsWriter.register('com.example.GreetingAutoConfiguration', elsewhere, null, null) + new File(elsewhere, IMPORTS).readLines() == ['com.example.GreetingAutoConfiguration'] + } + private List collectedWarnings = [] private List warnings() { diff --git a/grails-beans-dsl/src/test/groovy/org/grails/compiler/beans/OnGrailsEnvConditionSpec.groovy b/grails-beans-dsl/src/test/groovy/org/grails/compiler/beans/OnGrailsEnvConditionSpec.groovy index 5f58cf2b3cf..f334136b58c 100644 --- a/grails-beans-dsl/src/test/groovy/org/grails/compiler/beans/OnGrailsEnvConditionSpec.groovy +++ b/grails-beans-dsl/src/test/groovy/org/grails/compiler/beans/OnGrailsEnvConditionSpec.groovy @@ -114,4 +114,84 @@ class OnGrailsEnvConditionSpec extends Specification { expect: !condition.matches(context, metadataFor('development')) } + + void "a value that is not a list of environment names means no match"() { + given: + def metadata = Stub(AnnotatedTypeMetadata) { + getAnnotationAttributes(ConditionalOnGrailsEnv.name) >> ([value: 'development'] as Map) + } + + expect: + !condition.matches(contextFor('development', loaderThrowing(new ClassNotFoundException('grails.util.Environment'))), metadata) + } + + void "a null class loader resolves the environment through the default loader"() { + given: "Grails is on this classpath, so the environment it reports is the one that decides" + String name = grails.util.Environment.current.name + + expect: + condition.matches(contextFor('some-other-env', null), metadataFor(name)) + !condition.matches(contextFor(name, null), metadataFor('not-' + name)) + } + + void "the environment Grails reports wins over the grails.env property"() { + given: + String name = grails.util.Environment.current.name + + expect: + condition.matches(contextFor('production', getClass().classLoader), metadataFor(name)) + } + + void "an Environment that has no current environment falls back to the property"() { + given: + def context = contextFor('development', environmentLoader('static Object getCurrent() { null }')) + + expect: + condition.matches(context, metadataFor('development')) + !condition.matches(context, metadataFor('production')) + } + + void "a current environment without a name falls back to the property"() { + given: + def context = contextFor('development', environmentLoader( + 'static Object getCurrent() { new Environment() }\nObject getName() { null }')) + + expect: + condition.matches(context, metadataFor('development')) + } + + void "an environment name from Grails is lower-cased before it is compared"() { + given: + def context = contextFor(null, environmentLoader( + 'static Object getCurrent() { new Environment() }\nObject getName() { \'STAGING\' }')) + + expect: + condition.matches(context, metadataFor('staging')) + !condition.matches(context, metadataFor('production')) + } + + void "an Environment that fails when asked for its name falls back to the property"() { + given: + def context = contextFor('development', environmentLoader( + 'static Object getCurrent() { new Environment() }\nObject getName() { throw new IllegalStateException(\'boom\') }')) + + expect: + condition.matches(context, metadataFor('development')) + } + + /** + * A loader that answers {@code grails.util.Environment} with a class compiled from the given + * members, so the condition meets an Environment that behaves as the test needs. + */ + private ClassLoader environmentLoader(String members) { + GroovyClassLoader compiler = new GroovyClassLoader(getClass().classLoader) + Class fake = compiler.parseClass("package grails.util\nclass Environment {\n${members}\n}") + new ClassLoader(getClass().classLoader) { + @Override + Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + name == 'grails.util.Environment' ? fake : super.loadClass(name, resolve) + } + } + } + } diff --git a/grails-common/src/test/groovy/org/apache/grails/common/aot/RegistrableTypesSpec.groovy b/grails-common/src/test/groovy/org/apache/grails/common/aot/RegistrableTypesSpec.groovy index ad6f82e7b8b..5fc0a204083 100644 --- a/grails-common/src/test/groovy/org/apache/grails/common/aot/RegistrableTypesSpec.groovy +++ b/grails-common/src/test/groovy/org/apache/grails/common/aot/RegistrableTypesSpec.groovy @@ -18,7 +18,10 @@ */ package org.apache.grails.common.aot +import java.lang.reflect.Modifier + import org.springframework.asm.ClassWriter +import org.springframework.asm.ConstantDynamic import org.springframework.asm.Handle import org.springframework.asm.Label import org.springframework.asm.MethodVisitor @@ -95,6 +98,135 @@ class RegistrableTypesSpec extends Specification { shape << Shape.values() } + void 'the utility class cannot be instantiated by accident'() { + given: + def constructor = RegistrableTypes.getDeclaredConstructor() + + expect: + Modifier.isPrivate(constructor.modifiers) + + when: + constructor.accessible = true + constructor.newInstance() + + then: + noExceptionThrown() + } + + void 'bytecode is rejected when the absent type is named by a supertype'() { + expect: + !RegistrableTypes.referencesLoad(declaring(superName, interfaces), loader) + + where: + superName | interfaces + 'com/example/Absent' | null + 'java/lang/Object' | ['com/example/Absent'] as String[] + } + + void 'bytecode naming only types that load by its supertypes is accepted'() { + expect: + RegistrableTypes.referencesLoad(declaring('java/lang/Number', ['java/lang/Runnable'] as String[]), loader) + } + + void 'bytecode is rejected when an instruction names the absent type'() { + expect: + !RegistrableTypes.referencesLoad(bodyOf(instruction), loader) + + where: + instruction << [ + { MethodVisitor m -> m.visitTypeInsn(Opcodes.NEW, 'com/example/Absent') }, + { MethodVisitor m -> m.visitTypeInsn(Opcodes.ANEWARRAY, 'com/example/Absent') }, + { MethodVisitor m -> m.visitTypeInsn(Opcodes.CHECKCAST, '[[Lcom/example/Absent;') }, + { MethodVisitor m -> + m.visitFieldInsn(Opcodes.GETSTATIC, 'com/example/Absent', 'VALUE', 'Ljava/lang/Object;') + }, + { MethodVisitor m -> + m.visitFieldInsn(Opcodes.GETSTATIC, 'java/lang/Object', 'VALUE', '[Lcom/example/Absent;') + }, + { MethodVisitor m -> + m.visitMethodInsn(Opcodes.INVOKESTATIC, 'com/example/Absent', 'call', '()V', false) + }, + { MethodVisitor m -> m.visitMultiANewArrayInsn('[[Lcom/example/Absent;', 2) }, + { MethodVisitor m -> m.visitLdcInsn(Type.getType('[Lcom/example/Absent;')) }, + { MethodVisitor m -> m.visitLdcInsn(Type.getMethodType('(Lcom/example/Absent;)V')) }, + { MethodVisitor m -> m.visitLdcInsn(handle('com/example/Absent', '()V')) } + ] + } + + void 'bytecode naming only types that load in those places is accepted'() { + expect: + RegistrableTypes.referencesLoad(bodyOf(instruction), loader) + + where: + instruction << [ + { MethodVisitor m -> m.visitTypeInsn(Opcodes.NEW, 'java/lang/Object') }, + { MethodVisitor m -> m.visitTypeInsn(Opcodes.CHECKCAST, '[[Ljava/lang/String;') }, + { MethodVisitor m -> m.visitTypeInsn(Opcodes.CHECKCAST, '[I') }, + { MethodVisitor m -> + m.visitFieldInsn(Opcodes.GETSTATIC, 'java/lang/Boolean', 'TRUE', 'Ljava/lang/Boolean;') + }, + { MethodVisitor m -> m.visitMultiANewArrayInsn('[[Ljava/lang/String;', 2) }, + { MethodVisitor m -> m.visitLdcInsn(Type.getMethodType('(Ljava/lang/String;)V')) }, + { MethodVisitor m -> m.visitLdcInsn('plain constant') }, + { MethodVisitor m -> m.visitLdcInsn(Type.getType(Number)) }, + { MethodVisitor m -> m.visitLdcInsn(handle('java/lang/Number', '()V')) } + ] + } + + void 'bytecode is rejected when a dynamic constant names the absent type'() { + expect: + !RegistrableTypes.referencesLoad(bodyOf { MethodVisitor m -> + m.visitLdcInsn(dynamic(descriptor, owner, argument)) + }, loader) + + where: + descriptor | owner | argument + 'Lcom/example/Absent;' | 'java/lang/Object' | Type.getType(Number) + 'Ljava/lang/Object;' | 'com/example/Absent' | Type.getType(Number) + 'Ljava/lang/Object;' | 'java/lang/Object' | Type.getObjectType('com/example/Absent') + } + + void 'a dynamic constant naming only types that load is accepted'() { + expect: + RegistrableTypes.referencesLoad(bodyOf { MethodVisitor m -> + m.visitLdcInsn(dynamic('Ljava/lang/Object;', 'java/lang/Object', Type.getType(Number))) + }, loader) + } + + private static Handle handle(String owner, String descriptor) { + new Handle(Opcodes.H_INVOKESTATIC, owner, 'call', descriptor, false) + } + + private static ConstantDynamic dynamic(String descriptor, String bootstrapOwner, Object argument) { + new ConstantDynamic('constant', descriptor, + new Handle(Opcodes.H_INVOKESTATIC, bootstrapOwner, 'bootstrap', + '(Ljava/lang/invoke/MethodHandles$Lookup;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/Object;', + false), + argument) + } + + private InputStream declaring(String superName, String[] interfaces) { + ClassWriter writer = new ClassWriter(0) + writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, + 'org/apache/grails/common/aot/Written', null, superName, interfaces) + writer.visitEnd() + new ByteArrayInputStream(writer.toByteArray()) + } + + private InputStream bodyOf(Closure instruction) { + ClassWriter writer = new ClassWriter(0) + writer.visit(Opcodes.V21, Opcodes.ACC_PUBLIC | Opcodes.ACC_ABSTRACT, + 'org/apache/grails/common/aot/Written', null, 'java/lang/Object', null) + MethodVisitor method = writer.visitMethod(Opcodes.ACC_PUBLIC, 'body', '()V', null, null) + method.visitCode() + instruction.call(method) + method.visitInsn(Opcodes.RETURN) + method.visitMaxs(0, 0) + method.visitEnd() + writer.visitEnd() + new ByteArrayInputStream(writer.toByteArray()) + } + /** The places a class file can name a type, one per shape. */ private enum Shape { FIELD_TYPE, ARRAY_FIELD_TYPE, RETURN_TYPE, PARAMETER_TYPE, THROWN_TYPE, diff --git a/grails-common/src/test/groovy/org/apache/grails/common/reflect/ReflectionUtilsSpec.groovy b/grails-common/src/test/groovy/org/apache/grails/common/reflect/ReflectionUtilsSpec.groovy index fe31ce55264..f662caf466a 100644 --- a/grails-common/src/test/groovy/org/apache/grails/common/reflect/ReflectionUtilsSpec.groovy +++ b/grails-common/src/test/groovy/org/apache/grails/common/reflect/ReflectionUtilsSpec.groovy @@ -22,6 +22,10 @@ import java.beans.PropertyDescriptor import java.lang.reflect.Field import java.lang.reflect.Method import java.lang.reflect.Modifier +import java.util.concurrent.ConcurrentHashMap + +import org.springframework.asm.ClassWriter +import org.springframework.asm.Opcodes import org.springframework.beans.BeanUtils @@ -171,4 +175,81 @@ class ReflectionUtilsSpec extends Specification { and: !ReflectionUtils.warnOnNonPublicClass(null) } + + void 'the utility class cannot be instantiated by accident'() { + given: + def constructor = ReflectionUtils.getDeclaredConstructor() + + expect: + Modifier.isPrivate(constructor.modifiers) + + when: + constructor.accessible = true + constructor.newInstance() + + then: + noExceptionThrown() + } + + void 'a static read method is resolved even when an instance is supplied'() { + given: + Method readMethod = Integer.getMethod('parseInt', String) + + when: + Method invokable = ReflectionUtils.resolveInvokableReadMethod(readMethod, Integer, 'ignored') + + then: + invokable.invoke(null, '7') == 7 + } + + void 'a static field is readable even when an instance is supplied'() { + given: + Field field = Integer.getDeclaredField('MAX_VALUE') + + expect: + ReflectionUtils.tryMakeReadable(field, 'ignored') + } + + void 'a field of a class in a module that does not open its package is skipped, not failed'() { + given: 'the table of a ConcurrentHashMap, in a java.base package the test JVM does not open' + Field field = ConcurrentHashMap.getDeclaredField('table') + + expect: + !ReflectionUtils.tryMakeReadable(field, new ConcurrentHashMap()) + } + + void 'a lambda class is synthetic and is not reported'() { + given: + Runnable lambda = () -> { } + + expect: + !ReflectionUtils.warnOnNonPublicClass(lambda.getClass()) + } + + void 'the set of reported classes is bounded'() { + given: 'more distinct non-public classes than the bookkeeping is willing to remember' + def loader = new ClassLoader(getClass().classLoader) { + Class define(String name) { + ClassWriter writer = new ClassWriter(0) + writer.visit(Opcodes.V21, 0, name.replace('.', '/'), null, 'java/lang/Object', null) + writer.visitEnd() + byte[] bytes = writer.toByteArray() + defineClass(name, bytes, 0, bytes.length) + } + } + int reported = 0 + Class last = null + + when: + for (int i = 0; i < 1100; i++) { + last = loader.define("org.apache.grails.common.reflect.generated.Bounded${i}") + if (ReflectionUtils.warnOnNonPublicClass(last)) { + reported++ + } + } + + then: 'the bound is respected, and a class past it is not reported' + reported == 1024 + !ReflectionUtils.warnOnNonPublicClass(last) + } } From 65242a803beb2b07358d4a4d5f7475c5dbba1076 Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Tue, 13 Oct 2026 06:03:05 -0500 Subject: [PATCH 06/14] test(gradle-model): cover the converted grails-gradle model classes and fix ClassPathResource.equals - add specs for GrailsNameUtils, DefaultCommandLine and the file-resolving resources (file, jar and http), and extend the parser, loader, resource-utils and IO-utils specs - ClassPathResource.equals read the other resource's classLoader through its getter, which falls back to the class's loader, while reading its own field; two resources created from the same class were therefore never equal. Compare fields on both sides, as the Java original did. Co-Authored-By: Claude Sonnet 5.5 --- .../io/support/ClassPathResource.groovy | 4 +- .../grails/util/CollectionUtilsSpec.groovy | 14 + .../grails/util/GrailsNameUtilsSpec.groovy | 394 ++++++++++++++++++ .../test/groovy/grails/util/HolderSpec.groovy | 34 ++ .../parsing/CommandLineParserSpec.groovy | 94 +++++ .../parsing/DefaultCommandLineSpec.groovy | 276 ++++++++++++ .../AbstractFileResolvingResourceSpec.groovy | 197 +++++++++ .../io/support/ClassPathResourceSpec.groovy | 66 +++ .../support/DefaultResourceLoaderSpec.groovy | 40 ++ .../io/support/FileSystemResourceSpec.groovy | 45 ++ .../io/support/GrailsResourceUtilsSpec.groovy | 306 ++++++++++++++ .../io/support/SpringIOUtilsSpec.groovy | 144 +++++++ .../grails/io/support/UrlResourceSpec.groovy | 59 +++ 13 files changed, 1671 insertions(+), 2 deletions(-) create mode 100644 grails-gradle/model/src/test/groovy/grails/util/GrailsNameUtilsSpec.groovy create mode 100644 grails-gradle/model/src/test/groovy/org/grails/build/parsing/DefaultCommandLineSpec.groovy create mode 100644 grails-gradle/model/src/test/groovy/org/grails/io/support/AbstractFileResolvingResourceSpec.groovy diff --git a/grails-gradle/model/src/main/groovy/org/grails/io/support/ClassPathResource.groovy b/grails-gradle/model/src/main/groovy/org/grails/io/support/ClassPathResource.groovy index e5c3ab6bf80..ca923d39d52 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/io/support/ClassPathResource.groovy +++ b/grails-gradle/model/src/main/groovy/org/grails/io/support/ClassPathResource.groovy @@ -218,8 +218,8 @@ class ClassPathResource extends AbstractFileResolvingResource { if (obj instanceof ClassPathResource) { ClassPathResource otherRes = (ClassPathResource) obj return (path.equals(otherRes.path) && - nullSafeEquals(classLoader, otherRes.classLoader) && - nullSafeEquals(clazz, otherRes.clazz)) + nullSafeEquals(classLoader, otherRes.@classLoader) && + nullSafeEquals(clazz, otherRes.@clazz)) } return false } diff --git a/grails-gradle/model/src/test/groovy/grails/util/CollectionUtilsSpec.groovy b/grails-gradle/model/src/test/groovy/grails/util/CollectionUtilsSpec.groovy index 048626f3608..b941789fcec 100644 --- a/grails-gradle/model/src/test/groovy/grails/util/CollectionUtilsSpec.groovy +++ b/grails-gradle/model/src/test/groovy/grails/util/CollectionUtilsSpec.groovy @@ -85,4 +85,18 @@ class CollectionUtilsSpec extends Specification { CollectionUtils.getOrCreateChildMap(parent, 'foo') == [:] } + void 'the utility class cannot be instantiated'() { + given: + def constructor = CollectionUtils.getDeclaredConstructor() + + expect: + java.lang.reflect.Modifier.isPrivate(constructor.modifiers) + + when: + constructor.accessible = true + + then: + constructor.newInstance() != null + } + } diff --git a/grails-gradle/model/src/test/groovy/grails/util/GrailsNameUtilsSpec.groovy b/grails-gradle/model/src/test/groovy/grails/util/GrailsNameUtilsSpec.groovy new file mode 100644 index 00000000000..9a50e8eaddb --- /dev/null +++ b/grails-gradle/model/src/test/groovy/grails/util/GrailsNameUtilsSpec.groovy @@ -0,0 +1,394 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package grails.util + +import spock.lang.Specification +import spock.lang.Unroll + +class GrailsNameUtilsSpec extends Specification { + + @Unroll + void 'getSetterName(#property) is #expected'() { + expect: + GrailsNameUtils.getSetterName(property) == expected + + where: + property | expected + 'name' | 'setName' + 'aName' | 'setaName' + 'x' | 'setX' + 'URL' | 'setURL' + } + + @Unroll + void 'getGetterName(#property) is #expected'() { + expect: + GrailsNameUtils.getGetterName(property) == expected + + where: + property | expected + 'name' | 'getName' + 'aName' | 'getaName' + 'x' | 'getX' + } + + void 'getClassName combines a logical name and a trailing name'() { + expect: + GrailsNameUtils.getClassName('person', 'Controller') == 'PersonController' + GrailsNameUtils.getClassName('person', null) == 'Person' + GrailsNameUtils.getClassName('person') == 'Person' + } + + @Unroll + void 'getClassName rejects the blank logical name [#name]'() { + when: + GrailsNameUtils.getClassName(name, 'Controller') + + then: + thrown(IllegalArgumentException) + + where: + name << [null, '', ' '] + } + + void 'getFullClassName strips proxy and closure suffixes'() { + expect: + GrailsNameUtils.getFullClassName('com.acme.Foo$_closure1') == 'com.acme.Foo' + GrailsNameUtils.getFullClassName('com.acme.Foo') == 'com.acme.Foo' + GrailsNameUtils.getFullClassName(Map.Entry) == 'java.util.Map' + } + + @Unroll + void 'getClassNameRepresentation(#name) is #expected'() { + expect: + GrailsNameUtils.getClassNameRepresentation(name) == expected + + where: + name | expected + null | '' + '' | '' + 'foo-bar baz' | 'FooBarBaz' + 'foo' | 'Foo' + 'a' | 'A' + 'foo__bar' | 'Foo__bar' + } + + void 'getLogicalName removes the trailing name from a class or class name'() { + expect: + GrailsNameUtils.getLogicalName('PersonController', 'Controller') == 'Person' + GrailsNameUtils.getLogicalName('com.acme.PersonController', 'Controller') == 'Person' + GrailsNameUtils.getLogicalName('Person', 'Controller') == 'Person' + GrailsNameUtils.getLogicalName('PersonController', '') == 'PersonController' + GrailsNameUtils.getLogicalName('PersonController', null) == 'PersonController' + GrailsNameUtils.getLogicalName(ArrayList, 'List') == 'Array' + } + + void 'getLogicalPropertyName lower-cases the logical name'() { + expect: + GrailsNameUtils.getLogicalPropertyName('PersonController', 'Controller') == 'person' + GrailsNameUtils.getLogicalPropertyName('AController', 'Controller') == 'a' + GrailsNameUtils.getLogicalPropertyName('Person', 'Controller') == 'person' + GrailsNameUtils.getLogicalPropertyName('Person', null) == 'person' + } + + @Unroll + void 'getPropertyName(#name) is #expected'() { + expect: + GrailsNameUtils.getPropertyName(name) == expected + GrailsNameUtils.getPropertyNameRepresentation(name) == expected + + where: + name | expected + 'MyClass' | 'myClass' + 'URL' | 'URL' + 'a.b.Foo' | 'foo' + 'Foo Bar' | 'fooBar' + '' | '' + 'X' | 'x' + } + + void 'getPropertyName of a class uses its short name'() { + expect: + GrailsNameUtils.getPropertyName(StringBuilder) == 'stringBuilder' + GrailsNameUtils.getPropertyNameRepresentation(Map.Entry) == 'map$Entry' + } + + @Unroll + void 'getPropertyNameForLowerCaseHyphenSeparatedName(#name) is #expected'() { + expect: + GrailsNameUtils.getPropertyNameForLowerCaseHyphenSeparatedName(name) == expected + + where: + name | expected + 'foo-bar' | 'fooBar' + 'foo' | 'foo' + 'foo--bar-baz' | 'fooBarBaz' + '' | '' + } + + void 'getNameFromScript converts a hyphenated script name to a class name'() { + expect: + GrailsNameUtils.getNameFromScript('my-funk-grails-script') == 'MyFunkGrailsScript' + GrailsNameUtils.getNameFromScript('script') == 'Script' + } + + @Unroll + void 'getShortName and getPackageName split #name'() { + expect: + GrailsNameUtils.getShortName(name) == shortName + GrailsNameUtils.getPackageName(name) == packageName + + where: + name | shortName | packageName + 'a.b.C' | 'C' | 'a.b' + 'C' | 'C' | '' + 'a.C' | 'C' | 'a' + } + + void 'getShortName of a class drops the package'() { + expect: + GrailsNameUtils.getShortName(String) == 'String' + } + + @Unroll + void 'getScriptName(#name) is #expected'() { + expect: + GrailsNameUtils.getScriptName(name) == expected + + where: + name | expected + 'MyFunkyGrailsScript' | 'my-funky-grails-script' + 'MyFunkyScript.groovy' | 'my-funky-script' + 'script' | 'script' + } + + void 'getScriptName tolerates null'() { + expect: + GrailsNameUtils.getScriptName((String) null) == null + GrailsNameUtils.getScriptName((Class) null) == null + GrailsNameUtils.getScriptName(StringBuilder) == 'string-builder' + } + + @Unroll + void 'getPluginName(#descriptor) is #expected'() { + expect: + GrailsNameUtils.getPluginName(descriptor) == expected + + where: + descriptor | expected + 'DbUtilsGrailsPlugin.groovy' | 'db-utils' + 'FooGrailsPlugin.groovy' | 'foo' + '' | '' + null | null + } + + void 'getPluginName rejects a name that is not a plugin descriptor'() { + when: + GrailsNameUtils.getPluginName('Foo.groovy') + + then: + IllegalArgumentException e = thrown() + e.message == 'Plugin descriptor name is not valid: Foo.groovy' + } + + @Unroll + void 'getNaturalName(#name) is #expected'() { + expect: + GrailsNameUtils.getNaturalName(name) == expected + + where: + name | expected + 'firstName' | 'First Name' + 'MyFunkyGrailsScript' | 'My Funky Grails Script' + 'URL' | 'URL' + 'address1' | 'Address1' + 'com.acme.lastName' | 'Last Name' + 'a' | 'A' + } + + @Unroll + void 'isBlank([#value]) is #expected'() { + expect: + GrailsNameUtils.isBlank(value) == expected + + where: + value | expected + null | true + '' | true + ' \t' | true + 'a' | false + ' a ' | false + } + + @Unroll + void 'isValidJavaPackage(#name) is #expected'() { + expect: + GrailsNameUtils.isValidJavaPackage(name) == expected + + where: + name | expected + 'a.b.c' | true + 'abc' | true + 'a..b' | false + '1a.b' | false + 'a.b-c' | false + '' | false + null | false + } + + @Unroll + void 'isValidJavaIdentifier(#name) is #expected'() { + expect: + GrailsNameUtils.isValidJavaIdentifier(name) == expected + + where: + name | expected + 'foo_1' | true + '$bar' | true + '1foo' | false + 'a-b' | false + ' ' | false + null | false + } + + void 'getPropertyNameConvention names scalars after their type'() { + expect: + GrailsNameUtils.getPropertyNameConvention(null) == null + GrailsNameUtils.getPropertyNameConvention('text') == 'string' + GrailsNameUtils.getPropertyNameConvention('text', 'X') == 'stringX' + GrailsNameUtils.getPropertyNameConvention(new StringBuilder()) == 'stringBuilder' + } + + void 'getPropertyNameConvention names collections after their element type'() { + expect: + GrailsNameUtils.getPropertyNameConvention([]) == 'emptyCollection' + GrailsNameUtils.getPropertyNameConvention(['a']) == 'stringList' + GrailsNameUtils.getPropertyNameConvention(['a'], 'X') == 'stringXList' + GrailsNameUtils.getPropertyNameConvention(['a'] as Set) == 'stringSet' + GrailsNameUtils.getPropertyNameConvention(new ArrayDeque(['a'])) == 'stringCollection' + } + + void 'getPropertyNameConvention names maps after their value type'() { + expect: + GrailsNameUtils.getPropertyNameConvention([:]) == 'emptyMap' + GrailsNameUtils.getPropertyNameConvention([k: 'v']) == 'stringMap' + GrailsNameUtils.getPropertyNameConvention([k: null]) == null + } + + void 'getPropertyNameConvention names arrays after their component type'() { + expect: + GrailsNameUtils.getPropertyNameConvention([1, 2] as int[]) == 'intArray' + GrailsNameUtils.getPropertyNameConvention(['a'] as String[], 'X') == 'stringXArray' + } + + @Unroll + void 'getPropertyForGetter(#getter, #returnType) is #expected'() { + expect: + GrailsNameUtils.getPropertyForGetter(getter, returnType) == expected + + where: + getter | returnType | expected + 'getFoo' | 'String' | 'foo' + 'getURL' | 'String' | 'URL' + 'getA' | 'String' | 'a' + 'getaName' | 'String' | 'aName' + 'get_x' | 'String' | '_x' + 'get' | 'String' | null + 'getfoo' | 'String' | null + 'get1' | 'String' | null + 'getx' | 'String' | null + 'isActive' | 'boolean' | 'active' + 'isActive' | 'String' | null + 'foo' | 'String' | null + '' | 'String' | null + null | 'String' | null + } + + void 'getPropertyForGetter defaults to a boolean return type'() { + expect: + GrailsNameUtils.getPropertyForGetter('isActive') == 'active' + GrailsNameUtils.getPropertyForGetter('getName') == 'name' + GrailsNameUtils.getPropertyForGetter('isActive', String) == null + GrailsNameUtils.getPropertyForGetter('isActive', boolean) == 'active' + } + + @Unroll + void 'isGetter(#name, #returnType, #args) is #expected'() { + expect: + GrailsNameUtils.isGetter(name, returnType, args as Class[]) == expected + + where: + name | returnType | args | expected + 'getFoo' | String | [] | true + 'getFoo' | String | [String] | false + 'getX' | String | [] | true + 'getaProp' | String | [] | true + 'getx' | String | [] | false + 'get2other' | String | [] | false + 'get' | String | [] | false + 'isActive' | boolean | [] | true + 'isActive' | String | [] | false + 'isactive' | boolean | [] | false + 'foo' | String | [] | false + '' | String | [] | false + null | String | [] | false + 'getFoo' | String | null | false + } + + void 'the deprecated isGetter overload assumes a boolean return type'() { + expect: + GrailsNameUtils.isGetter('isActive', [] as Class[]) + GrailsNameUtils.isGetter('getActive', [] as Class[]) + !GrailsNameUtils.isGetter('getActive', [String] as Class[]) + } + + @Unroll + void 'getPropertyForSetter(#setter) is #expected'() { + expect: + GrailsNameUtils.getPropertyForSetter(setter) == expected + + where: + setter | expected + 'setFoo' | 'foo' + 'setURL' | 'URL' + 'set' | null + 'setfoo' | null + 'getFoo' | null + '' | null + null | null + } + + @Unroll + void 'convertValidPropertyMethodSuffix(#suffix) is #expected'() { + expect: + GrailsNameUtils.convertValidPropertyMethodSuffix(suffix) == expected + + where: + suffix | expected + '' | null + '1a' | null + 'A' | 'a' + 'a' | null + 'Name' | 'name' + 'aName' | 'aName' + '_x' | '_x' + 'name' | null + } + +} diff --git a/grails-gradle/model/src/test/groovy/grails/util/HolderSpec.groovy b/grails-gradle/model/src/test/groovy/grails/util/HolderSpec.groovy index 3d2857b28c4..44879f22589 100644 --- a/grails-gradle/model/src/test/groovy/grails/util/HolderSpec.groovy +++ b/grails-gradle/model/src/test/groovy/grails/util/HolderSpec.groovy @@ -62,4 +62,38 @@ class HolderSpec extends Specification { holder.get(true) == 'hello' } + void 'a secondary lookup is consulted before the singleton'() { + given: + Holder holder = new Holder('secondary') { + @Override + protected String lookupSecondary() { + 'from-secondary' + } + } + holder.set('stored') + + expect: + holder.get() == 'stored' + + when: + holder.set(null) + + then: + holder.get() == 'from-secondary' + holder.get(true) == 'from-secondary' + } + + void 'values are keyed by the current class loader of the holder'() { + given: + Holder first = new Holder<>('first') + Holder second = new Holder<>('second') + + when: + first.set('one') + + then: + first.get() == 'one' + second.get() == null + } + } diff --git a/grails-gradle/model/src/test/groovy/org/grails/build/parsing/CommandLineParserSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/build/parsing/CommandLineParserSpec.groovy index e4a52ea0940..1eb14b0a8e8 100644 --- a/grails-gradle/model/src/test/groovy/org/grails/build/parsing/CommandLineParserSpec.groovy +++ b/grails-gradle/model/src/test/groovy/org/grails/build/parsing/CommandLineParserSpec.groovy @@ -406,4 +406,98 @@ class CommandLineParserSpec extends Specification { then: thrown ParseException } + + void 'translateCommandline returns no tokens for a null or empty string'() { + expect: + CommandLineParser.translateCommandline(null).length == 0 + CommandLineParser.translateCommandline('').length == 0 + } + + void 'translateCommandline keeps empty quoted tokens and collapses repeated spaces'() { + expect: + CommandLineParser.translateCommandline('a b') == ['a', 'b'] as String[] + CommandLineParser.translateCommandline("a '' b") == ['a', '', 'b'] as String[] + CommandLineParser.translateCommandline('a "x y" b') == ['a', 'x y', 'b'] as String[] + CommandLineParser.translateCommandline("""a "it's" b""") == ['a', "it's", 'b'] as String[] + } + + void 'the most recently created command line is the current one'() { + when: + CommandLine parsed = new CommandLineParser().parse('run-app') + + then: + CommandLineParser.currentCommandLine.is(parsed) + } + + void 'parseString with a command name treats every token as an argument'() { + when: + CommandLine commandLine = new CommandLineParser().parseString('run-app', 'dev --flag value') + + then: + commandLine.commandName == 'run-app' + commandLine.remainingArgs == ['dev'] + commandLine.optionValue('flag') == 'value' + } + + void 'declared options are recognised with and without a value'() { + given: + CommandLineParser parser = new CommandLineParser() + parser.addOption('verbose', 'Verbose output') + parser.addOption('level', 'The level') + + when: + CommandLine commandLine = parser.parse('run-app', '--verbose', '-level=3') + + then: + commandLine.hasOption('verbose') + commandLine.optionValue('verbose') == true + commandLine.optionValue('level') == '3' + commandLine.undeclaredOptions.isEmpty() + } + + void 'a bare dash and null or blank arguments are ignored'() { + when: + CommandLine commandLine = new CommandLineParser().parse([null, ' ', '-', 'run-app', '', 'arg'] as String[]) + + then: + commandLine.commandName == 'run-app' + commandLine.remainingArgs == ['arg'] + commandLine.undeclaredOptions.isEmpty() + } + + void 'surrounding double quotes are removed from an argument'() { + when: + CommandLine commandLine = new CommandLineParser().parse('run-app', '"quoted"') + + then: + commandLine.remainingArgs == ['quoted'] + } + + void 'environment shortcuts only apply to the first argument position'() { + when: + CommandLine commandLine = new CommandLineParser().parse('prod', 'run-app', 'dev') + + then: + commandLine.environment == 'production' + commandLine.commandName == 'run-app' + commandLine.remainingArgs == ['dev'] + } + + void 'an option name containing a space is rejected whether or not it has a value'() { + when: + new CommandLineParser().parse('run-app', '-bad name=value') + + then: + thrown(ParseException) + } + + void 'ParseException keeps its message or cause'() { + given: + Throwable cause = new IllegalStateException('boom') + + expect: + new ParseException('msg').message == 'msg' + new ParseException(cause).cause.is(cause) + } + } diff --git a/grails-gradle/model/src/test/groovy/org/grails/build/parsing/DefaultCommandLineSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/build/parsing/DefaultCommandLineSpec.groovy new file mode 100644 index 00000000000..b60fa0faab7 --- /dev/null +++ b/grails-gradle/model/src/test/groovy/org/grails/build/parsing/DefaultCommandLineSpec.groovy @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package org.grails.build.parsing + +import spock.lang.Specification + +import grails.util.Environment + +class DefaultCommandLineSpec extends Specification { + + private String originalEnv + private String originalDefault + + void setup() { + originalEnv = System.getProperty(Environment.KEY) + originalDefault = System.getProperty(Environment.DEFAULT) + System.clearProperty(Environment.KEY) + System.clearProperty(Environment.DEFAULT) + } + + void cleanup() { + restore(Environment.KEY, originalEnv) + restore(Environment.DEFAULT, originalDefault) + } + + private static void restore(String key, String value) { + if (value == null) { + System.clearProperty(key) + } + else { + System.setProperty(key, value) + } + } + + void 'a new command line has no command, arguments or options'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + + expect: + commandLine.commandName == null + commandLine.remainingArgs == [] + commandLine.remainingArgsArray.length == 0 + commandLine.remainingArgsString == '' + commandLine.undeclaredOptions == [:] + commandLine.systemProperties.isEmpty() + commandLine.rawArguments == null + !commandLine.environmentSet + !commandLine.hasOption('anything') + commandLine.optionValue('anything') == null + commandLine.lastOption() == null + } + + void 'the environment defaults to development and is flagged as defaulted'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + + expect: + commandLine.environment == Environment.DEVELOPMENT.name + System.getProperty(Environment.KEY) == Environment.DEVELOPMENT.name + System.getProperty(Environment.DEFAULT) == 'true' + } + + void 'the default environment honours an already set system property'() { + given: + System.setProperty(Environment.KEY, 'custom') + + expect: + new DefaultCommandLine().environment == 'custom' + } + + void 'an explicit environment wins and is published as a system property'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + + when: + commandLine.setEnvironment('production') + + then: + commandLine.environmentSet + commandLine.environment == 'production' + System.getProperty(Environment.KEY) == 'production' + System.getProperty(Environment.DEFAULT) == 'false' + } + + void 'the environment for a command comes from the command defaults'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + commandLine.setCommand(command) + + expect: + commandLine.environment == expected + commandLine.lookupEnvironmentForCommand() == expected + + where: + command | expected + 'war' | 'production' + 'test-app' | 'test' + 'run-app' | 'development' + } + + void 'a command without a default environment falls back to the system property'() { + given: + System.setProperty(Environment.KEY, 'custom') + DefaultCommandLine commandLine = new DefaultCommandLine() + commandLine.setCommandName('run-app') + + expect: + commandLine.lookupEnvironmentForCommand() == 'custom' + commandLine.environment == 'custom' + } + + void 'setCommandName records the command, and refresh-dependencies also becomes an option'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + + when: + commandLine.setCommandName('run-app') + + then: + commandLine.commandName == 'run-app' + !commandLine.hasOption(CommandLine.REFRESH_DEPENDENCIES_ARGUMENT) + + when: + commandLine.setCommandName(CommandLine.REFRESH_DEPENDENCIES_ARGUMENT) + + then: + commandLine.commandName == CommandLine.REFRESH_DEPENDENCIES_ARGUMENT + commandLine.hasOption(CommandLine.REFRESH_DEPENDENCIES_ARGUMENT) + commandLine.optionValue(CommandLine.REFRESH_DEPENDENCIES_ARGUMENT) == true + } + + void 'declared options report their value, defaulting to true'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + commandLine.addDeclaredOption('flag', new Option('flag', 'a flag')) + commandLine.addDeclaredOption('level', new Option('level', 'a level'), '3') + + expect: + commandLine.hasOption('flag') + commandLine.optionValue('flag') == true + commandLine.optionValue('level') == '3' + commandLine.declaredOptions.level.option.name == 'level' + commandLine.declaredOptions.level.value == '3' + } + + void 'undeclared options report their value, defaulting to true'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + commandLine.addUndeclaredOption('flag') + commandLine.addUndeclaredOption('name', 'value') + + expect: + commandLine.hasOption('flag') + commandLine.optionValue('flag') == true + commandLine.optionValue('name') == 'value' + commandLine.undeclaredOptions == [flag: true, name: 'value'] + } + + void 'declared options take precedence over undeclared ones of the same name'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + commandLine.addUndeclaredOption('mode', 'undeclared') + commandLine.addDeclaredOption('mode', new Option('mode', 'the mode'), 'declared') + + expect: + commandLine.optionValue('mode') == 'declared' + } + + void 'lastOption returns the most recently added undeclared option'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + commandLine.addUndeclaredOption('first') + commandLine.addUndeclaredOption('second', 'x') + + expect: + commandLine.lastOption().key == 'second' + commandLine.lastOption().value == 'x' + } + + void 'remaining arguments are exposed as a list, array and strings'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + commandLine.addRemainingArg('one') + commandLine.addRemainingArg('two') + + expect: + commandLine.remainingArgs == ['one', 'two'] + commandLine.remainingArgsArray == ['one', 'two'] as String[] + commandLine.remainingArgsString == 'one two' + commandLine.remainingArgsLineSeparated == 'one\ntwo' + commandLine.remainingArgsWithOptionsString == 'one two' + } + + void 'the string with options renders flags and valued options'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + commandLine.addRemainingArg('arg') + commandLine.addUndeclaredOption('flag') + commandLine.addUndeclaredOption('name', 'value') + commandLine.addUndeclaredOption('off', Boolean.FALSE) + + expect: + commandLine.remainingArgsWithOptionsString == 'arg -flag -name=value -off=false' + commandLine.remainingArgsString == 'arg' + } + + void 'system properties are stored, and grails.env also selects the environment'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + + when: + commandLine.addSystemProperty('foo', 'bar') + + then: + commandLine.systemProperties.getProperty('foo') == 'bar' + !commandLine.environmentSet + + when: + commandLine.addSystemProperty(Environment.KEY, 'test') + + then: + commandLine.systemProperties.getProperty(Environment.KEY) == 'test' + commandLine.environmentSet + commandLine.environment == 'test' + } + + void 'raw arguments are kept as given'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + String[] args = ['a', '-b'] as String[] + + when: + commandLine.setRawArguments(args) + + then: + commandLine.rawArguments.is(args) + } + + void 'parseNew carries the existing state into a freshly parsed command line'() { + given: + DefaultCommandLine commandLine = new DefaultCommandLine() + commandLine.addSystemProperty('foo', 'bar') + commandLine.addUndeclaredOption('existing', 'yes') + commandLine.addDeclaredOption('declared', new Option('declared', 'd')) + + when: + CommandLine parsed = commandLine.parseNew(['run-app', '-more', 'arg'] as String[]) + + then: + !parsed.is(commandLine) + parsed.commandName == 'run-app' + parsed.remainingArgs == [] + parsed.systemProperties.getProperty('foo') == 'bar' + parsed.optionValue('existing') == 'yes' + parsed.optionValue('declared') == true + parsed.optionValue('more') == 'arg' + !commandLine.hasOption('more') + } + +} diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/AbstractFileResolvingResourceSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/AbstractFileResolvingResourceSpec.groovy new file mode 100644 index 00000000000..9b7588e0866 --- /dev/null +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/AbstractFileResolvingResourceSpec.groovy @@ -0,0 +1,197 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * 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. + */ +package org.grails.io.support + +import java.util.jar.JarEntry +import java.util.jar.JarOutputStream + +import com.sun.net.httpserver.HttpExchange +import com.sun.net.httpserver.HttpServer +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.TempDir + +class AbstractFileResolvingResourceSpec extends Specification { + + @TempDir + File dir + + @Shared + HttpServer server + + @Shared + int port + + void setupSpec() { + server = HttpServer.create(new InetSocketAddress(InetAddress.loopbackAddress, 0), 0) + server.createContext('/ok') { HttpExchange exchange -> + exchange.responseHeaders.add('Last-Modified', 'Thu, 01 Jan 2015 00:00:00 GMT') + if (exchange.requestMethod == 'HEAD') { + exchange.responseHeaders.add('Content-Length', '5') + exchange.sendResponseHeaders(200, -1) + } + else { + exchange.sendResponseHeaders(200, 5) + exchange.responseBody.write('hello'.bytes) + } + exchange.close() + } + server.createContext('/missing') { HttpExchange exchange -> + exchange.sendResponseHeaders(404, -1) + exchange.close() + } + server.createContext('/error') { HttpExchange exchange -> + exchange.sendResponseHeaders(500, -1) + exchange.close() + } + server.start() + port = server.address.port + } + + void cleanupSpec() { + server.stop(0) + } + + private UrlResource http(String path) { + new UrlResource("http://${InetAddress.loopbackAddress.hostAddress}:${port}${path}".toString()) + } + + private File fileWith(String name, String content) { + File file = new File(dir, name) + file.text = content + file + } + + void 'a file resource resolves, reads and measures the underlying file'() { + given: + File file = fileWith('data.txt', 'hello') + UrlResource resource = new UrlResource(file.toURI().toURL()) + + expect: + resource.exists() + resource.readable + resource.file == file + resource.contentLength() == 5 + resource.lastModified() == file.lastModified() + } + + void 'a missing file is neither existing nor readable'() { + given: + UrlResource resource = new UrlResource(new File(dir, 'nope.txt').toURI().toURL()) + + expect: + !resource.exists() + !resource.readable + } + + void 'a directory exists but is not readable'() { + given: + UrlResource resource = new UrlResource(dir.toURI().toURL()) + + expect: + resource.exists() + !resource.readable + } + + void 'a resource outside the file system cannot be resolved to a file'() { + when: + new UrlResource('http://localhost/x').file + + then: + FileNotFoundException e = thrown() + e.message.contains('does not reside in the file system') + } + + void 'readable swallows the failure to resolve a file for a class path resource that does not exist'() { + expect: + !new ClassPathResource('no/such/resource.txt').readable + !new ClassPathResource('no/such/resource.txt').exists() + } + + void 'exists reflects the status of an http resource'() { + expect: + http('/ok').exists() + !http('/missing').exists() + !http('/error').exists() + } + + void 'an unreachable http resource does not exist'() { + given: + int unusedPort + new ServerSocket(0).withCloseable { unusedPort = it.localPort } + + expect: + !new UrlResource("http://${InetAddress.loopbackAddress.hostAddress}:${unusedPort}/x".toString()).exists() + } + + void 'an http resource is readable'() { + expect: + http('/ok').readable + } + + void 'contentLength and lastModified come from the http response headers'() { + expect: + http('/ok').contentLength() == 5 + http('/ok').lastModified() == 1420070400000L + } + + void 'a resource inside a jar reports a length and exists'() { + given: + File jar = new File(dir, 'sample.jar') + new JarOutputStream(new FileOutputStream(jar)).withCloseable { JarOutputStream out -> + out.putNextEntry(new JarEntry('inside.txt')) + out.write('jar content'.bytes) + out.closeEntry() + } + UrlResource resource = new UrlResource("jar:${jar.toURI()}!/inside.txt".toString()) + + expect: + resource.exists() + resource.readable + resource.contentLength() == 11 + resource.inputStream.text == 'jar content' + resource.getFileForLastModifiedCheck() == jar + } + + void 'the file used for the last modified check is the file itself for a file resource'() { + given: + File file = fileWith('plain.txt', 'x') + + expect: + new UrlResource(file.toURI().toURL()).getFileForLastModifiedCheck() == file + } + + void 'a class path resource inside a jar is read through the jar and not as a file'() { + given: + ClassPathResource resource = new ClassPathResource('spock/lang/Specification.class') + + expect: + resource.exists() + resource.readable + resource.contentLength() > 0 + resource.getFileForLastModifiedCheck().name.endsWith('.jar') + + when: + resource.file + + then: + thrown(FileNotFoundException) + } + +} diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/ClassPathResourceSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/ClassPathResourceSpec.groovy index f3b16debfd3..aa7286d542d 100644 --- a/grails-gradle/model/src/test/groovy/org/grails/io/support/ClassPathResourceSpec.groovy +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/ClassPathResourceSpec.groovy @@ -102,4 +102,70 @@ class ClassPathResourceSpec extends Specification { ((ClassPathResource) relative).getPath() == 'org/grails/bar.txt' } + void 'a resource relative to a class resolves against the class package'() { + given: + ClassPathResource resource = new ClassPathResource('ClassPathResourceSpec.class', ClassPathResourceSpec) + + expect: + resource.exists() + resource.path == 'ClassPathResourceSpec.class' + resource.classLoader.is(ClassPathResourceSpec.classLoader) + resource.description == 'class path resource [org/grails/io/support/ClassPathResourceSpec.class]' + resource.inputStream.withCloseable { it.read() } != -1 + resource.URL.toString().endsWith('org/grails/io/support/ClassPathResourceSpec.class') + resource.createRelative('UrlResourceSpec.class').exists() + } + + void 'a missing resource cannot be resolved to a URL'() { + when: + new ClassPathResource('missing.txt', ClassPathResourceSpec).URL + + then: + FileNotFoundException e = thrown() + e.message.contains('cannot be resolved to URL because it does not exist') + } + + void 'a class relative missing resource does not exist and cannot be opened'() { + given: + ClassPathResource resource = new ClassPathResource('missing.txt', ClassPathResourceSpec) + + expect: + !resource.exists() + + when: + resource.inputStream + + then: + thrown(FileNotFoundException) + } + + void 'a class path file resolves to a File and URI'() { + given: + ClassPathResource resource = new ClassPathResource('org/grails/io/support/ClassPathResourceSpec.class') + + expect: + resource.file.exists() + resource.URI == resource.file.toURI() + resource.contentLength() == resource.file.length() + resource.lastModified() == resource.file.lastModified() + resource.readable + } + + void 'the description of a class loader resource names only the path'() { + expect: + new ClassPathResource('a/b.txt').description == 'class path resource [a/b.txt]' + } + + void 'resources with the same class and path are equal and share a hash code'() { + given: + ClassPathResource first = new ClassPathResource('a.txt', ClassPathResourceSpec) + ClassPathResource second = new ClassPathResource('a.txt', ClassPathResourceSpec) + + expect: + first == second + first.hashCode() == second.hashCode() + first != new ClassPathResource('a.txt', UrlResourceSpec) + first != new ClassPathResource('a.txt') + } + } diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/DefaultResourceLoaderSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/DefaultResourceLoaderSpec.groovy index e5380a54e6b..edee40efa2c 100644 --- a/grails-gradle/model/src/test/groovy/org/grails/io/support/DefaultResourceLoaderSpec.groovy +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/DefaultResourceLoaderSpec.groovy @@ -82,4 +82,44 @@ class DefaultResourceLoaderSpec extends Specification { loader.getClassLoader().is(custom) } + void 'the default class loader falls back to this library when the thread has none'() { + given: + ClassLoader original = Thread.currentThread().contextClassLoader + + when: + Thread.currentThread().contextClassLoader = null + + then: + DefaultResourceLoader.defaultClassLoader.is(DefaultResourceLoader.classLoader) + + cleanup: + Thread.currentThread().contextClassLoader = original + } + + void 'a loader whose class loader was cleared uses the default class loader again'() { + given: + DefaultResourceLoader loader = new DefaultResourceLoader(new URLClassLoader(new URL[0])) + + when: + loader.classLoader = null + + then: + loader.classLoader.is(DefaultResourceLoader.defaultClassLoader) + } + + void 'plain path resources resolve relative to their own location'() { + given: + Resource resource = new DefaultResourceLoader().getResource('org/grails/io/support/ClassPathResourceSpec.class') + + when: + Resource sibling = resource.createRelative('DefaultResourceLoaderSpec.class') + + then: + resource.exists() + resource.pathWithinContext == 'org/grails/io/support/ClassPathResourceSpec.class' + sibling.exists() + sibling.filename == 'DefaultResourceLoaderSpec.class' + sibling.pathWithinContext == 'org/grails/io/support/DefaultResourceLoaderSpec.class' + } + } diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/FileSystemResourceSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/FileSystemResourceSpec.groovy index 652ebf2995b..db00529158c 100644 --- a/grails-gradle/model/src/test/groovy/org/grails/io/support/FileSystemResourceSpec.groovy +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/FileSystemResourceSpec.groovy @@ -113,4 +113,49 @@ class FileSystemResourceSpec extends Specification { relative.getFilename() == 'bar.txt' } + void 'readable is true for a file and false for a directory or a missing file'() { + given: + File file = File.createTempFile('fsr', '.txt') + file.deleteOnExit() + + expect: + new FileSystemResource(file).readable + !new FileSystemResource(file.parentFile).readable + !new FileSystemResource(new File(file.parentFile, 'does-not-exist-' + System.nanoTime())).readable + } + + void 'the resource exposes the underlying file as a URL, URI and File'() { + given: + File file = File.createTempFile('fsr', '.txt') + file.deleteOnExit() + file.text = 'abcd' + FileSystemResource resource = new FileSystemResource(file) + + expect: + resource.URL == file.toURI().toURL() + resource.URI == file.toURI() + resource.file == file + resource.contentLength() == 4 + resource.lastModified() == file.lastModified() + resource.description == "file [${file.absolutePath}]".toString() + resource.toString() == file.toString() + resource.hashCode() == new FileSystemResource(file).hashCode() + resource.path == GrailsResourceUtils.cleanPath(file.path) + } + + void 'writable and output stream operate on the underlying file'() { + given: + File file = File.createTempFile('fsr', '.txt') + file.deleteOnExit() + FileSystemResource resource = new FileSystemResource(file) + + when: + resource.outputStream.withCloseable { it.write('written'.bytes) } + + then: + resource.writable + !new FileSystemResource(file.parentFile).writable + file.text == 'written' + } + } diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/GrailsResourceUtilsSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/GrailsResourceUtilsSpec.groovy index 00db77632de..81cc3d1816a 100644 --- a/grails-gradle/model/src/test/groovy/org/grails/io/support/GrailsResourceUtilsSpec.groovy +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/GrailsResourceUtilsSpec.groovy @@ -133,4 +133,310 @@ class GrailsResourceUtilsSpec extends Specification { '/project/grails-app/controllers/com/example/BookController.groovy').find() } + @Unroll + void 'getClassName(#path) is #expected'() { + expect: + GrailsResourceUtils.getClassName(path) == expected + + where: + path | expected + '/p/grails-app/domain/com/acme/Book.groovy' | 'com.acme.Book' + '/p/grails-app/controllers/BookController.groovy' | 'BookController' + '/p/grails-app/conf/spring/MyBeans.groovy' | 'MyBeans' + '/p/src/main/groovy/com/acme/Util.groovy' | 'com.acme.Util' + '/p/src/main/java/com/acme/Util.java' | 'com.acme.Util' + '/p/src/test/groovy/com/acme/UtilSpec.groovy' | 'com.acme.UtilSpec' + '/p/src/test/java/com/acme/UtilTest.java' | 'com.acme.UtilTest' + '/p/src/test/functional/com/acme/FuncSpec.groovy' | 'com.acme.FuncSpec' + 'C:\\p\\grails-app\\services\\com\\acme\\Svc.groovy' | 'com.acme.Svc' + '/p/other/Unknown.groovy' | null + '/p/readme.txt' | null + } + + void 'getClassName of a resource uses the absolute file path'() { + expect: + GrailsResourceUtils.getClassName(new FileSystemResource('/p/grails-app/domain/com/acme/Book.groovy')) == 'com.acme.Book' + } + + void 'getClassName of a resource that is not a file is null'() { + expect: + GrailsResourceUtils.getClassName(new UrlResource('http://example.com/grails-app/domain/Book.groovy')) == null + } + + void 'getClassNameForClassFile removes the root, separators and extension'() { + expect: + GrailsResourceUtils.getClassNameForClassFile('/root/', '/root/com/acme/Foo.class') == 'com.acme.Foo' + GrailsResourceUtils.getClassNameForClassFile(null, 'com/acme/Foo.class') == 'com.acme.Foo' + GrailsResourceUtils.getClassNameForClassFile(null, 'com\\acme\\Foo.class') == 'com.acme.Foo' + } + + void 'getFile resolves a file URL and rejects other protocols'() { + expect: + GrailsResourceUtils.getFile(new URL('file:/tmp/a.txt'), 'desc') == new File('/tmp/a.txt') + GrailsResourceUtils.getFile(new URL('file:/tmp/a b.txt'), 'desc') == new File('/tmp/a b.txt') + + when: + GrailsResourceUtils.getFile(new URL('http://example.com/a.txt'), 'the resource') + + then: + FileNotFoundException e = thrown() + e.message.startsWith('the resource cannot be resolved to absolute file path') + } + + void 'getFile falls back to the URL file when the URL is not a valid URI'() { + expect: + GrailsResourceUtils.getFile(new URL('file:/tmp/a|b.txt'), 'desc') == new File('/tmp/a|b.txt') + } + + void 'getFile resolves a file URI and rejects other schemes'() { + expect: + GrailsResourceUtils.getFile(new URI('file:/tmp/a.txt'), 'desc') == new File('/tmp/a.txt') + GrailsResourceUtils.getFile(new URI('file:/tmp/b.txt')) == new File('/tmp/b.txt') + + when: + GrailsResourceUtils.getFile(new URI('http://example.com/a.txt')) + + then: + FileNotFoundException e = thrown() + e.message.startsWith('URI cannot be resolved to absolute file path') + } + + void 'toURI escapes spaces'() { + expect: + GrailsResourceUtils.toURI('file:/tmp/a b.txt') == new URI('file:/tmp/a%20b.txt') + GrailsResourceUtils.toURI(new URL('file:/tmp/a b.txt')) == new URI('file:/tmp/a%20b.txt') + } + + void 'isFileURL also accepts the vfs family of protocols'() { + expect: + GrailsResourceUtils.isFileURL(urlWithProtocol('vfs', '/tmp/x')) + GrailsResourceUtils.isFileURL(urlWithProtocol('vfszip', '/tmp/x')) + !GrailsResourceUtils.isFileURL(urlWithProtocol('other', '/tmp/x')) + } + + void 'isJarURL accepts a code-source URL only when it points inside a jar'() { + expect: + GrailsResourceUtils.isJarURL(urlWithProtocol('code-source', '/x/y.jar!/z')) + !GrailsResourceUtils.isJarURL(urlWithProtocol('code-source', '/x/y')) + } + + @Unroll + void 'extractJarFileURL(#protocol:#spec) is #expected'() { + expect: + GrailsResourceUtils.extractJarFileURL(urlWithProtocol(protocol, spec)).toString() == expected + + where: + protocol | spec | expected + 'jar' | 'file:/tmp/a.jar!/x/y.txt' | 'file:/tmp/a.jar' + 'jar' | 'C:/mypath/myjar.jar!/x.txt' | 'file:/C:/mypath/myjar.jar' + 'jar' | '/mypath/myjar.jar!/x.txt' | 'file:/mypath/myjar.jar' + 'http' | '//example.com/a.jar' | 'http://example.com/a.jar' + } + + void 'useCachesIfNecessary switches caching off for ordinary connections'() { + given: + URLConnection connection = new URL('file:/tmp/x').openConnection() + connection.useCaches = true + + when: + GrailsResourceUtils.useCachesIfNecessary(connection) + + then: + !connection.useCaches + } + + @Unroll + void 'cleanPath(#path) is #expected'() { + expect: + GrailsResourceUtils.cleanPath(path) == expected + + where: + path | expected + '/foo/bar' | '/foo/bar' + '../foo' | '../foo' + 'foo/../../bar' | '../bar' + 'file:core/../core/io/R.class' | 'file:core/io/R.class' + 'c:\\dir\\file.txt' | 'c:/dir/file.txt' + '' | '' + 'a//b' | 'a//b' + } + + @Unroll + void 'applyRelativePath(#path, #relative) is #expected'() { + expect: + GrailsResourceUtils.applyRelativePath(path, relative) == expected + + where: + path | relative | expected + 'a/b/c.txt' | 'd.txt' | 'a/b/d.txt' + 'a/b/c.txt' | '/d.txt' | 'a/b/d.txt' + 'c.txt' | 'd.txt' | 'd.txt' + } + + @Unroll + void 'isGrailsPath(#path) is #expected'() { + expect: + GrailsResourceUtils.isGrailsPath(path) == expected + GrailsResourceUtils.isGrailsPath(path) == expected + + where: + path | expected + '/p/grails-app/views/book/Show.groovy' | true + '/p/grails-app/conf/spring/Beans.groovy' | true + '/p/src/main/groovy/com/acme/Util.groovy' | false + '/p/readme.txt' | false + } + + @Unroll + void 'isProjectSource(#path) is #expected'() { + expect: + GrailsResourceUtils.isProjectSource(path) == expected + + where: + path | expected + '/p/src/main/groovy/com/acme/Util.groovy' | true + '/p/grails-app/domain/com/acme/Book.groovy' | true + '/p/readme.txt' | false + } + + void 'isProjectSource and isGrailsResource inspect the resource URL'() { + expect: + GrailsResourceUtils.isProjectSource(new UrlResource('file:/p/src/main/groovy/a/B.groovy')) + GrailsResourceUtils.isProjectSource(new UrlResource('file:/p/FooGrailsPlugin.groovy')) + !GrailsResourceUtils.isProjectSource(new UrlResource('file:/p/readme.txt')) + GrailsResourceUtils.isGrailsResource(new UrlResource('file:/p/grails-app/views/a/B.groovy')) + GrailsResourceUtils.isGrailsResource(new UrlResource('file:/p/FooGrailsPlugin.groovy')) + !GrailsResourceUtils.isGrailsResource(new UrlResource('file:/p/readme.txt')) + } + + void 'isProjectSource and isGrailsResource are false for a resource without a URL'() { + given: + Resource missing = new ClassPathResource('no/such/thing.groovy') + + expect: + !GrailsResourceUtils.isProjectSource(missing) + !GrailsResourceUtils.isGrailsResource(missing) + } + + void 'getAppDir and getViewsDir find the grails-app directory of a file resource'() { + given: + Resource controller = new FileSystemResource('/p/grails-app/controllers/a/BookController.groovy') + + expect: + GrailsResourceUtils.getAppDir(controller).file.name == 'grails-app' + GrailsResourceUtils.getViewsDir(controller).path == '/p/grails-app/views' + } + + void 'getAppDir finds the grails-app directory of a non file resource'() { + given: + Resource view = new UrlResource('http://example.com/x/grails-app/views/a/b.gsp') + + expect: + GrailsResourceUtils.getAppDir(view) == new UrlResource('http://example.com/x/grails-app/') + } + + void 'getAppDir and getViewsDir are null when there is no grails-app directory or resource'() { + expect: + GrailsResourceUtils.getAppDir(null) == null + GrailsResourceUtils.getViewsDir(null) == null + GrailsResourceUtils.getAppDir(new FileSystemResource('/p/other/A.groovy')) == null + GrailsResourceUtils.getViewsDir(new FileSystemResource('/p/other/A.groovy')) == null + GrailsResourceUtils.getAppDir(new ClassPathResource('no/such/thing.groovy')) == null + } + + void 'getRelativeInsideWebInf locates a resource below WEB-INF'() { + expect: + GrailsResourceUtils.getRelativeInsideWebInf(new UrlResource('http://h/app/WEB-INF/grails-app/views/a.gsp')) == '/WEB-INF/grails-app/views/a.gsp' + GrailsResourceUtils.getRelativeInsideWebInf(new UrlResource('http://h/plugins/foo-1.0/grails-app/views/a.gsp')) == '/WEB-INF/plugins/foo-1.0/grails-app/views/a.gsp' + GrailsResourceUtils.getRelativeInsideWebInf(new UrlResource('http://h/x/grails-app/views/a.gsp')) == '/WEB-INF/grails-app/views/a.gsp' + GrailsResourceUtils.getRelativeInsideWebInf(new UrlResource('http://h/x/readme.txt')) == null + GrailsResourceUtils.getRelativeInsideWebInf(null) == null + GrailsResourceUtils.getRelativeInsideWebInf(new ClassPathResource('no/such/thing')) == null + } + + void 'getStaticResourcePathForResource maps plugin resources below the context path'() { + given: + Resource plugin = new UrlResource('http://h/plugins/foo-1.0/grails-app/assets/a.js') + + expect: + GrailsResourceUtils.getStaticResourcePathForResource(plugin, '/ctx') == '/ctx/plugins/foo-1.0' + GrailsResourceUtils.getStaticResourcePathForResource(plugin, '') == 'plugins/foo-1.0' + GrailsResourceUtils.getStaticResourcePathForResource(plugin, null) == 'plugins/foo-1.0' + GrailsResourceUtils.getStaticResourcePathForResource(new UrlResource('http://h/x/a.js'), '/ctx') == '/ctx' + GrailsResourceUtils.getStaticResourcePathForResource(null, '/ctx') == '/ctx' + GrailsResourceUtils.getStaticResourcePathForResource(null, null) == '' + GrailsResourceUtils.getStaticResourcePathForResource(new ClassPathResource('no/such/thing'), '/ctx') == '/ctx' + } + + void 'getPathFromRoot returns the file inside the artefact directory'() { + expect: + GrailsResourceUtils.getPathFromRoot('/p/grails-app/controllers/a/b/Foo.groovy') == 'a/b/Foo.groovy' + GrailsResourceUtils.getPathFromRoot('/p/readme.txt') == null + } + + void 'getPathFromBaseDir strips everything up to grails-app'() { + expect: + GrailsResourceUtils.getPathFromBaseDir('/p/grails-app/views/a.gsp') == 'views/a.gsp' + } + + void 'getPathFromBaseDir makes other paths relative to the base directory when there is one'() { + given: + File baseDir = grails.util.BuildSettings.BASE_DIR + + expect: + baseDir == null ? GrailsResourceUtils.getPathFromBaseDir('x/y.txt') == null : + GrailsResourceUtils.getPathFromBaseDir(new File(baseDir, 'x/y.txt').path) == 'x/y.txt' + } + + void 'getArtefactDirectory names the grails-app subdirectory'() { + expect: + GrailsResourceUtils.getArtefactDirectory('/p/grails-app/controllers/a/Foo.groovy') == 'controllers' + GrailsResourceUtils.getArtefactDirectory('/p/readme.txt') == null + GrailsResourceUtils.getArtefactDirectory(null) == null + } + + @Unroll + void 'appendPiecesForUri(#pieces) is #expected'() { + expect: + GrailsResourceUtils.appendPiecesForUri(pieces as String[]) == expected + + where: + pieces | expected + ['a', 'b'] | 'a/b' + ['a/', '/b'] | 'a/b' + ['a', null, 'b'] | 'a/b' + ['/a', '', 'b'] | '/a/b' + [] | '' + } + + void 'appendPiecesForUri of null is empty'() { + expect: + GrailsResourceUtils.appendPiecesForUri((String[]) null) == '' + } + + void 'instantiateFromFlatConfig creates the configured class or the default'() { + expect: + GrailsResourceUtils.instantiateFromFlatConfig([k: 'java.util.ArrayList'], 'k', 'java.util.HashMap') instanceof ArrayList + GrailsResourceUtils.instantiateFromFlatConfig([:], 'k', 'java.util.HashMap') instanceof HashMap + GrailsResourceUtils.instantiateFromFlatConfig([k: 42], 'k', 'java.util.HashMap') instanceof HashMap + } + + void 'instantiateFromConfig flattens the config object first'() { + given: + ConfigObject config = new ConfigObject() + config.my.impl = 'java.util.ArrayList' + + expect: + GrailsResourceUtils.instantiateFromConfig(config, 'my.impl', 'java.util.HashMap') instanceof ArrayList + GrailsResourceUtils.instantiateFromConfig(config, 'missing', 'java.util.HashMap') instanceof HashMap + } + + void 'instantiateFromFlatConfig fails for an unknown class'() { + when: + GrailsResourceUtils.instantiateFromFlatConfig([k: 'no.such.Type'], 'k', 'java.util.HashMap') + + then: + thrown(ClassNotFoundException) + } + } diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/SpringIOUtilsSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/SpringIOUtilsSpec.groovy index ac69111f954..b1b676d4155 100644 --- a/grails-gradle/model/src/test/groovy/org/grails/io/support/SpringIOUtilsSpec.groovy +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/SpringIOUtilsSpec.groovy @@ -311,4 +311,148 @@ class SpringIOUtilsSpec extends Specification { SpringIOUtils.addAll(['a', 'b'] as String[], ['c', 'd'] as String[]) == ['a', 'b', 'c', 'd'] } + void 'addAll rejects an array whose type cannot hold the other arrays elements'() { + when: + SpringIOUtils.addAll(['a'] as String[], [1] as Integer[]) + + then: + IllegalArgumentException e = thrown() + e.message == 'Cannot store java.lang.Integer in an array of java.lang.String' + } + + void 'copyAll copies every resource below the base into the target directory'() { + given: + File source = new File(tempFileDir, 'src') + source.mkdirs() + new File(source, 'a.txt').text = 'A' + new File(source, 'b.txt').text = 'B' + File target = new File(tempFileDir, 'target') + target.mkdirs() + + when: + SpringIOUtils.copyAll(new FileSystemResource(source), [new FileSystemResource(new File(source, 'a.txt')), + new FileSystemResource(new File(source, 'b.txt'))] as Resource[], target) + + then: + new File(target, 'a.txt').text == 'A' + new File(target, 'b.txt').text == 'B' + } + + void 'copy(Resource, File) writes the resource to the file and reports the size'() { + given: + File in = new File(tempFileDir, 'in.txt') + in.text = 'resource' + File out = new File(tempFileDir, 'out.txt') + + expect: + SpringIOUtils.copy(new FileSystemResource(in), out) == 8 + out.text == 'resource' + } + + void 'copy(File, File) reports the number of bytes copied'() { + given: + File in = new File(tempFileDir, 'in2.txt') + in.text = 'twelve bytes' + File out = new File(tempFileDir, 'out2.txt') + + expect: + SpringIOUtils.copy(in, out) == 12 + } + + void 'copy(InputStream, OutputStream) copies, flushes and closes both streams'() { + given: + boolean inClosed = false + boolean outClosed = false + InputStream input = new ByteArrayInputStream('payload'.bytes) { + @Override + void close() { + inClosed = true + } + } + ByteArrayOutputStream output = new ByteArrayOutputStream() { + @Override + void close() { + outClosed = true + } + } + + expect: + SpringIOUtils.copy(input, output) == 7 + output.toString() == 'payload' + inClosed + outClosed + } + + void 'copy(InputStream, OutputStream) still closes the streams when they fail to close'() { + given: + InputStream input = new ByteArrayInputStream('x'.bytes) { + @Override + void close() { + throw new IOException('in') + } + } + OutputStream output = new ByteArrayOutputStream() { + @Override + void close() { + throw new IOException('out') + } + } + + expect: + SpringIOUtils.copy(input, output) == 1 + } + + void 'copy(byte[], OutputStream) writes and closes the stream even when closing fails'() { + given: + ByteArrayOutputStream output = new ByteArrayOutputStream() { + @Override + void close() { + throw new IOException('out') + } + } + + when: + SpringIOUtils.copy('bytes'.bytes, (OutputStream) output) + + then: + output.toString() == 'bytes' + } + + void 'copyToByteArray(InputStream) reads everything and closes the stream'() { + expect: + SpringIOUtils.copyToByteArray(new ByteArrayInputStream('abc'.bytes)) == 'abc'.bytes + } + + void 'copy(Reader, Writer) copies characters and closes both'() { + given: + StringWriter writer = new StringWriter() + + expect: + SpringIOUtils.copy(new StringReader('chars'), writer) == 5 + writer.toString() == 'chars' + } + + void 'copy methods refuse missing arguments'() { + when: + action() + + then: + thrown(AssertionError) + + where: + action << [ + { SpringIOUtils.copy((File) null, new File('x')) }, + { SpringIOUtils.copy(new File('x'), (File) null) }, + { SpringIOUtils.copy((Resource) null, new File('x')) }, + { SpringIOUtils.copy(new FileSystemResource('x'), (File) null) }, + { SpringIOUtils.copy((byte[]) null, new File('x')) }, + { SpringIOUtils.copy('x'.bytes, (File) null) }, + { SpringIOUtils.copy((InputStream) null, new ByteArrayOutputStream()) }, + { SpringIOUtils.copy(new ByteArrayInputStream(new byte[0]), (OutputStream) null) }, + { SpringIOUtils.copy((byte[]) null, (OutputStream) new ByteArrayOutputStream()) }, + { SpringIOUtils.copy('x'.bytes, (OutputStream) null) }, + { SpringIOUtils.copyToByteArray((File) null) } + ] + } + } diff --git a/grails-gradle/model/src/test/groovy/org/grails/io/support/UrlResourceSpec.groovy b/grails-gradle/model/src/test/groovy/org/grails/io/support/UrlResourceSpec.groovy index 490372ae693..cb955e93876 100644 --- a/grails-gradle/model/src/test/groovy/org/grails/io/support/UrlResourceSpec.groovy +++ b/grails-gradle/model/src/test/groovy/org/grails/io/support/UrlResourceSpec.groovy @@ -106,4 +106,63 @@ class UrlResourceSpec extends Specification { resource.toString() == resource.getDescription() } + void 'a resource can be created from a URI, a URL string or a URL'() { + given: + URI uri = new File('/tmp/x.txt').toURI() + + expect: + new UrlResource(uri).URL == uri.toURL() + new UrlResource(uri).URI.is(uri) || new UrlResource(uri).URI == uri + new UrlResource(uri.toString()).URL == uri.toURL() + new UrlResource(uri) == new UrlResource(uri.toURL()) + } + + void 'getURI derives the URI from the file when none was given'() { + given: + File file = new File('/tmp/derived.txt') + + expect: + new UrlResource(file.toURI().toURL()).URI == file.toURI() + } + + void 'getFile resolves through the URI when the resource was created from one'() { + given: + File file = new File('/tmp/viauri.txt') + + expect: + new UrlResource(file.toURI()).file == file + } + + void 'createRelative drops a leading slash and returns null for an unusable path'() { + given: + UrlResource base = new UrlResource('http://example.com/a/b.txt') + + expect: + base.createRelative('/c.txt').URL.toString() == 'http://example.com/a/c.txt' + base.createRelative('foo:bar') == null + } + + void 'hashCode is consistent with equals'() { + expect: + new UrlResource('http://example.com/a/../b').hashCode() == new UrlResource('http://example.com/b').hashCode() + } + + void 'getInputStream reads a file resource'() { + given: + File file = File.createTempFile('url-resource', '.txt') + file.deleteOnExit() + file.text = 'content' + + expect: + new UrlResource(file.toURI().toURL()).inputStream.text == 'content' + } + + void 'getInputStream fails for a resource that does not exist'() { + when: + new UrlResource(new File('/no/such/file.txt').toURI().toURL()).inputStream + + then: + thrown(IOException) + } + } From 580bd5e99e916438b508e24c155aff572d0d44eb Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Fri, 16 Oct 2026 19:28:37 -0500 Subject: [PATCH 07/14] refactor(build-tooling): keep DefaultCommandLine's semicolons to preserve rename history git's default 50% rename-similarity threshold is what GitHub's PR view and 'git log --follow'/'blame' use to link a converted file back to its original .java predecessor. DefaultCommandLine's full idiomatic conversion landed at 41% similarity (confirmed: GitHub shows delete+add, not a rename, losing file history per matrei's review comment on #16576). This keeps the method-body semicolons (76% similarity, confirmed rename- detected, confirmed git log --follow/blame trace through to the real pre-conversion commits and author) while still applying every other idiom fix: dropped the public modifier, the 7 unnecessary java.util.* imports, and the 4 non-interpolated double-quoted string literals CodeNarc also flags. @SuppressWarnings('UnnecessarySemicolon') marks this as a known, temporary, scoped exception pending the stacked groovyism-cleanup PR, which removes both the semicolons and this suppression. Co-Authored-By: Claude Sonnet 5 --- .../build/parsing/DefaultCommandLine.groovy | 154 +++++++++--------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy index c2e16df9fd5..719211c6ced 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy +++ b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy @@ -18,9 +18,6 @@ */ package org.grails.build.parsing -import groovy.transform.CompileStatic -import groovy.transform.PackageScope - import grails.util.Environment /** @@ -29,202 +26,205 @@ import grails.util.Environment * @author Graeme Rocher * @since 2.0 */ -@CompileStatic +// TODO(groovyism-cleanup): the semicolons below are intentionally kept so this +// conversion commit stays above git's rename-similarity threshold against the +// original DefaultCommandLine.java (76% with them, 45% without) and preserves +// file history. The stacked groovyism-cleanup PR removes them and this suppression. +@SuppressWarnings('UnnecessarySemicolon') class DefaultCommandLine implements CommandLine { - @PackageScope Properties systemProperties = new Properties() - @PackageScope LinkedHashMap undeclaredOptions = new LinkedHashMap<>() - @PackageScope LinkedHashMap declaredOptions = new LinkedHashMap<>() - @PackageScope List remainingArgs = new ArrayList<>() - private String environment - private String commandName - private String[] rawArguments + Properties systemProperties = new Properties(); + LinkedHashMap undeclaredOptions = new LinkedHashMap<>(); + LinkedHashMap declaredOptions = new LinkedHashMap<>(); + List remainingArgs = new ArrayList<>(); + private String environment; + private String commandName; + private String[] rawArguments; void addDeclaredOption(String name, Option option) { - addDeclaredOption(name, option, Boolean.TRUE) + addDeclaredOption(name, option, Boolean.TRUE); } void addUndeclaredOption(String option) { - undeclaredOptions.put(option, Boolean.TRUE) + undeclaredOptions.put(option, Boolean.TRUE); } void addUndeclaredOption(String option, Object value) { - undeclaredOptions.put(option, value) + undeclaredOptions.put(option, value); } void addDeclaredOption(String name, Option option, Object value) { - SpecifiedOption so = new SpecifiedOption() - so.option = option - so.value = value + SpecifiedOption so = new SpecifiedOption(); + so.option = option; + so.value = value; - declaredOptions.put(name, so) + declaredOptions.put(name, so); } @Override CommandLine parseNew(String[] args) { - DefaultCommandLine defaultCommandLine = new DefaultCommandLine() - defaultCommandLine.systemProperties.putAll(systemProperties) - defaultCommandLine.undeclaredOptions.putAll(undeclaredOptions) - defaultCommandLine.declaredOptions.putAll(declaredOptions) - CommandLineParser parser = new CommandLineParser() - return parser.parse(defaultCommandLine, args) + DefaultCommandLine defaultCommandLine = new DefaultCommandLine(); + defaultCommandLine.systemProperties.putAll(systemProperties); + defaultCommandLine.undeclaredOptions.putAll(undeclaredOptions); + defaultCommandLine.declaredOptions.putAll(declaredOptions); + CommandLineParser parser = new CommandLineParser(); + return parser.parse(defaultCommandLine, args); } void setEnvironment(String environment) { - this.environment = environment - System.setProperty(Environment.KEY, environment) + this.environment = environment; + System.setProperty(Environment.KEY, environment); } void setCommand(String name) { - commandName = name + commandName = name; } String getEnvironment() { - boolean useDefaultEnv = environment == null - String env + boolean useDefaultEnv = environment == null; + String env; if (useDefaultEnv && commandName != null) { - env = lookupEnvironmentForCommand() + env = lookupEnvironmentForCommand(); } else { - String fallbackEnv = System.getProperty(Environment.KEY) != null ? System.getProperty(Environment.KEY) : Environment.DEVELOPMENT.getName() - env = environment != null ? environment : fallbackEnv + String fallbackEnv = System.getProperty(Environment.KEY) != null ? System.getProperty(Environment.KEY) : Environment.DEVELOPMENT.getName(); + env = environment != null ? environment : fallbackEnv; } - System.setProperty(Environment.KEY, env) - System.setProperty(Environment.DEFAULT, String.valueOf(useDefaultEnv)) + System.setProperty(Environment.KEY, env); + System.setProperty(Environment.DEFAULT, String.valueOf(useDefaultEnv)); - return env + return env; } String lookupEnvironmentForCommand() { - String fallbackEnv = System.getProperty(Environment.KEY) != null ? System.getProperty(Environment.KEY) : Environment.DEVELOPMENT.getName() - String env = CommandLineParser.DEFAULT_ENVS.get(commandName) - return env == null ? fallbackEnv : env + String fallbackEnv = System.getProperty(Environment.KEY) != null ? System.getProperty(Environment.KEY) : Environment.DEVELOPMENT.getName(); + String env = CommandLineParser.DEFAULT_ENVS.get(commandName); + return env == null ? fallbackEnv : env; } boolean isEnvironmentSet() { - return environment != null + return environment != null; } void setCommandName(String cmd) { if (REFRESH_DEPENDENCIES_ARGUMENT.equals(cmd)) { - addUndeclaredOption(REFRESH_DEPENDENCIES_ARGUMENT) + addUndeclaredOption(REFRESH_DEPENDENCIES_ARGUMENT); } - commandName = cmd + commandName = cmd; } String getCommandName() { - return commandName + return commandName; } void addRemainingArg(String arg) { - remainingArgs.add(arg) + remainingArgs.add(arg); } List getRemainingArgs() { - return remainingArgs + return remainingArgs; } String[] getRemainingArgsArray() { - return remainingArgs.toArray(new String[remainingArgs.size()]) + return remainingArgs.toArray(new String[remainingArgs.size()]); } Properties getSystemProperties() { - return systemProperties + return systemProperties; } boolean hasOption(String name) { - return declaredOptions.containsKey(name) || undeclaredOptions.containsKey(name) + return declaredOptions.containsKey(name) || undeclaredOptions.containsKey(name); } Object optionValue(String name) { if (declaredOptions.containsKey(name)) { - SpecifiedOption specifiedOption = declaredOptions.get(name) - return specifiedOption.value + SpecifiedOption specifiedOption = declaredOptions.get(name); + return specifiedOption.value; } if (undeclaredOptions.containsKey(name)) { - return undeclaredOptions.get(name) + return undeclaredOptions.get(name); } - return null + return null; } @Override Map.Entry lastOption() { - final Iterator> i = undeclaredOptions.entrySet().iterator() + final Iterator> i = undeclaredOptions.entrySet().iterator(); while (i.hasNext()) { - Map.Entry next = i.next() + Map.Entry next = i.next(); if (!i.hasNext()) { - return next + return next; } } - return null + return null; } String getRemainingArgsString() { - return remainingArgsToString(' ', false) + return remainingArgsToString(' ', false); } @Override String getRemainingArgsWithOptionsString() { - return remainingArgsToString(' ', true) + return remainingArgsToString(' ', true); } String getRemainingArgsLineSeparated() { - return remainingArgsToString('\n', false) + return remainingArgsToString('\n', false); } private String remainingArgsToString(String separator, boolean includeOptions) { - StringBuilder sb = new StringBuilder() - String sep = '' - List args = new ArrayList<>(remainingArgs) + StringBuilder sb = new StringBuilder(); + String sep = ''; + List args = new ArrayList<>(remainingArgs); if (includeOptions) { for (Map.Entry entry : undeclaredOptions.entrySet()) { if (entry.getValue() instanceof Boolean && ((Boolean) entry.getValue())) { - args.add('-' + entry.getKey()) + args.add('-' + entry.getKey()); } else { - args.add('-' + entry.getKey() + '=' + entry.getValue()) + args.add('-' + entry.getKey() + '=' + entry.getValue()); } } } - for (String arg in args) { - sb.append(sep).append(arg) - sep = separator + for (String arg : args) { + sb.append(sep).append(arg); + sep = separator; } - return sb.toString() + return sb.toString(); } Map getUndeclaredOptions() { - return undeclaredOptions + return undeclaredOptions; } void addSystemProperty(String name, String value) { if (Environment.KEY.equals(name)) { - setEnvironment(value) + setEnvironment(value); } - systemProperties.put(name, value) + systemProperties.put(name, value); } void setRawArguments(String[] args) { - this.rawArguments = args + this.rawArguments = args; } @Override String[] getRawArguments() { - return rawArguments + return rawArguments; } static class SpecifiedOption { - private Option option - private Object value + private Option option; + private Object value; Option getOption() { - return option + return option; } Object getValue() { - return value + return value; } } - } From 72948a2abf7c266a1bec507f9468c1c9cacf81f3 Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Sat, 17 Oct 2026 21:16:23 -0400 Subject: [PATCH 08/14] test(core): TEMPORARY CI diagnostic for the hand-authored imports warning GlobalGrailsClassInjectorTransformationSpec's "a generated class missing from a hand-authored imports file is reported" fails on every GitHub-hosted runner (Ubuntu 21/25, macOS, Windows, Coverage) with an empty warning list, and passes locally in isolation, paired with every other base.dir-touching spec, and in the full suite at both local and CI fork counts. Static analysis rules out base.dir leakage (grails-core ships its own hand-authored imports file, so a leaked value would still warn), groovy.* property mutation, Spock parallelism and Groovy version drift (6.0.0 pinned on both sides). This inlines the compilation so the power assertion prints which branch of AutoConfigurationImportsWriter.register ran on the runner: whether the sibling was generated, whether the generated imports file was written (hand-authored file not found) or not (hand-authored branch taken), the collector's warning level and counts, and the directories involved. REVERT THIS COMMIT once the CI log has been read. Co-Authored-By: Claude Fable 5.1 --- ...ailsClassInjectorTransformationSpec.groovy | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/grails-core/src/test/groovy/org/grails/compiler/injection/GlobalGrailsClassInjectorTransformationSpec.groovy b/grails-core/src/test/groovy/org/grails/compiler/injection/GlobalGrailsClassInjectorTransformationSpec.groovy index f2618fe6209..4b88e966b17 100644 --- a/grails-core/src/test/groovy/org/grails/compiler/injection/GlobalGrailsClassInjectorTransformationSpec.groovy +++ b/grails-core/src/test/groovy/org/grails/compiler/injection/GlobalGrailsClassInjectorTransformationSpec.groovy @@ -540,9 +540,11 @@ class GlobalGrailsClassInjectorTransformationSpec extends Specification { handAuthored.text = 'com.elsewhere.FromAnotherJar\n' when: "it compiles" - def warnings = compileCollectingWarnings( - new File(tempDir, 'WarnOnceGrailsPlugin.groovy'), - ''' + // TEMPORARY CI DIAGNOSTIC (revert before merge): this feature fails only on GitHub-hosted + // runners with an empty warning list and cannot be reproduced locally. The compilation is + // inlined here so the power assertion below prints which branch of the registration ran. + def sourceFile = new File(tempDir, 'WarnOnceGrailsPlugin.groovy') + sourceFile.text = ''' @org.springframework.boot.autoconfigure.AutoConfiguration class WarnOnceGrailsPlugin extends grails.plugins.Plugin { def version = '1.0' @@ -550,12 +552,33 @@ class GlobalGrailsClassInjectorTransformationSpec extends Specification { bean('greeting', String) { 'hello' } } } - ''', - targetDir - ) + ''' + def cu = new CompilationUnit(new CompilerConfiguration(targetDirectory: targetDir)) + cu.addSource(sourceFile) + cu.compile(Phases.CANONICALIZATION) + def warnings = (cu.errorCollector.warnings ?: []).collect { it.message?.toString() ?: it.toString() } + def generatedImports = new File(targetDir, + 'META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports') + def diag = [ + baseDirProp: System.getProperty('base.dir'), + groovyWarningsProp: System.getProperty('groovy.warnings'), + disabledGlobalTransformsProp: System.getProperty('groovy.disabled.global.ast.transformations'), + warningLevel: cu.configuration.warningLevel, + javaIoTmpdir: System.getProperty('java.io.tmpdir'), + userDir: System.getProperty('user.dir'), + tempDir: tempDir.absolutePath, + targetDir: targetDir.absolutePath, + handAuthoredIsFile: handAuthored.isFile(), + siblingGenerated: cu.AST.getClass('WarnOnceAutoConfiguration') != null, + generatedImportsExists: generatedImports.exists(), + generatedImportsText: generatedImports.exists() ? generatedImports.text : null, + errorCount: cu.errorCollector.errorCount, + warningCount: cu.errorCollector.warningCount, + allWarnings: warnings, + ] then: "the entry that has to be added by hand is named" - warnings.any { it.contains('WarnOnceAutoConfiguration') && it.contains('AutoConfiguration.imports') } + warnings.any { it.contains('WarnOnceAutoConfiguration') && it.contains('AutoConfiguration.imports') } || diag == null and: "and the module's own file is left as the only one" !new File(targetDir, From 9b42e6ec8bbfbabecc8652c70d674f296fda8c0e Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Sat, 17 Oct 2026 22:53:34 -0400 Subject: [PATCH 09/14] fix(build-tooling): restore @CompileStatic and @PackageScope dropped by the rename-fix 580bd5e99e rebuilt DefaultCommandLine from the .java original, which carries no annotations, and so lost the @CompileStatic stage 1 had put on the class and the @PackageScope on systemProperties, undeclaredOptions, declaredOptions and remainingArgs. Without them the four fields became public Groovy properties with generated accessors and the class fell back to dynamic dispatch. Both are restored; similarity stays at R069. grails-gradle-model: compile, codenarc and 432 tests pass. Co-Authored-By: Claude Fable 5.1 --- .../grails/build/parsing/DefaultCommandLine.groovy | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy index 719211c6ced..9e2e5df48cc 100644 --- a/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy +++ b/grails-gradle/model/src/main/groovy/org/grails/build/parsing/DefaultCommandLine.groovy @@ -18,6 +18,9 @@ */ package org.grails.build.parsing +import groovy.transform.CompileStatic +import groovy.transform.PackageScope + import grails.util.Environment /** @@ -31,12 +34,13 @@ import grails.util.Environment // original DefaultCommandLine.java (76% with them, 45% without) and preserves // file history. The stacked groovyism-cleanup PR removes them and this suppression. @SuppressWarnings('UnnecessarySemicolon') +@CompileStatic class DefaultCommandLine implements CommandLine { - Properties systemProperties = new Properties(); - LinkedHashMap undeclaredOptions = new LinkedHashMap<>(); - LinkedHashMap declaredOptions = new LinkedHashMap<>(); - List remainingArgs = new ArrayList<>(); + @PackageScope Properties systemProperties = new Properties(); + @PackageScope LinkedHashMap undeclaredOptions = new LinkedHashMap<>(); + @PackageScope LinkedHashMap declaredOptions = new LinkedHashMap<>(); + @PackageScope List remainingArgs = new ArrayList<>(); private String environment; private String commandName; private String[] rawArguments; From 1c91abf5ae1e58934d293c79cad71be0316061b1 Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Sat, 17 Oct 2026 23:01:43 -0400 Subject: [PATCH 10/14] Revert "test(core): TEMPORARY CI diagnostic for the hand-authored imports warning" This reverts commit 72948a2abf7c266a1bec507f9468c1c9cacf81f3. --- ...ailsClassInjectorTransformationSpec.groovy | 37 ++++--------------- 1 file changed, 7 insertions(+), 30 deletions(-) diff --git a/grails-core/src/test/groovy/org/grails/compiler/injection/GlobalGrailsClassInjectorTransformationSpec.groovy b/grails-core/src/test/groovy/org/grails/compiler/injection/GlobalGrailsClassInjectorTransformationSpec.groovy index 4b88e966b17..f2618fe6209 100644 --- a/grails-core/src/test/groovy/org/grails/compiler/injection/GlobalGrailsClassInjectorTransformationSpec.groovy +++ b/grails-core/src/test/groovy/org/grails/compiler/injection/GlobalGrailsClassInjectorTransformationSpec.groovy @@ -540,11 +540,9 @@ class GlobalGrailsClassInjectorTransformationSpec extends Specification { handAuthored.text = 'com.elsewhere.FromAnotherJar\n' when: "it compiles" - // TEMPORARY CI DIAGNOSTIC (revert before merge): this feature fails only on GitHub-hosted - // runners with an empty warning list and cannot be reproduced locally. The compilation is - // inlined here so the power assertion below prints which branch of the registration ran. - def sourceFile = new File(tempDir, 'WarnOnceGrailsPlugin.groovy') - sourceFile.text = ''' + def warnings = compileCollectingWarnings( + new File(tempDir, 'WarnOnceGrailsPlugin.groovy'), + ''' @org.springframework.boot.autoconfigure.AutoConfiguration class WarnOnceGrailsPlugin extends grails.plugins.Plugin { def version = '1.0' @@ -552,33 +550,12 @@ class GlobalGrailsClassInjectorTransformationSpec extends Specification { bean('greeting', String) { 'hello' } } } - ''' - def cu = new CompilationUnit(new CompilerConfiguration(targetDirectory: targetDir)) - cu.addSource(sourceFile) - cu.compile(Phases.CANONICALIZATION) - def warnings = (cu.errorCollector.warnings ?: []).collect { it.message?.toString() ?: it.toString() } - def generatedImports = new File(targetDir, - 'META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports') - def diag = [ - baseDirProp: System.getProperty('base.dir'), - groovyWarningsProp: System.getProperty('groovy.warnings'), - disabledGlobalTransformsProp: System.getProperty('groovy.disabled.global.ast.transformations'), - warningLevel: cu.configuration.warningLevel, - javaIoTmpdir: System.getProperty('java.io.tmpdir'), - userDir: System.getProperty('user.dir'), - tempDir: tempDir.absolutePath, - targetDir: targetDir.absolutePath, - handAuthoredIsFile: handAuthored.isFile(), - siblingGenerated: cu.AST.getClass('WarnOnceAutoConfiguration') != null, - generatedImportsExists: generatedImports.exists(), - generatedImportsText: generatedImports.exists() ? generatedImports.text : null, - errorCount: cu.errorCollector.errorCount, - warningCount: cu.errorCollector.warningCount, - allWarnings: warnings, - ] + ''', + targetDir + ) then: "the entry that has to be added by hand is named" - warnings.any { it.contains('WarnOnceAutoConfiguration') && it.contains('AutoConfiguration.imports') } || diag == null + warnings.any { it.contains('WarnOnceAutoConfiguration') && it.contains('AutoConfiguration.imports') } and: "and the module's own file is left as the only one" !new File(targetDir, From ef0ba33d56b151c4d2ff2b31c9857e90474e74d7 Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Sun, 18 Oct 2026 10:15:42 -0400 Subject: [PATCH 11/14] test: expect the platform's separator from projectPath in the file system interaction specs Backport of #16664 (ff463ccb57 on 7.2.x, merged up to 9.0.x) to the three 10.0.x copies of the spec, so the Windows shards pass until the 9.0.x merge-up reaches 10.0.x; the lines are identical, so that merge stays clean. Co-Authored-By: Claude Fable 5.1 --- .../grails/dev/commands/io/FileSystemInteractionImplSpec.groovy | 2 +- .../grails/core/cli/io/FileSystemInteractionImplSpec.groovy | 2 +- .../profile/commands/io/FileSystemInteractionImplSpec.groovy | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/grails-core-cli-legacy/src/test/groovy/grails/dev/commands/io/FileSystemInteractionImplSpec.groovy b/grails-core-cli-legacy/src/test/groovy/grails/dev/commands/io/FileSystemInteractionImplSpec.groovy index ddd22a28f09..ee515626144 100644 --- a/grails-core-cli-legacy/src/test/groovy/grails/dev/commands/io/FileSystemInteractionImplSpec.groovy +++ b/grails-core-cli-legacy/src/test/groovy/grails/dev/commands/io/FileSystemInteractionImplSpec.groovy @@ -66,7 +66,7 @@ class FileSystemInteractionImplSpec extends Specification { } then: - interaction.projectPath(missing) == 'nested/missing.txt' + interaction.projectPath(missing) == new File('nested', 'missing.txt').path interaction.projectPath('missing.txt') == 'missing.txt' !interaction.resource(missing).exists() pendingInteraction.file('build.gradle') == new File(pending, 'build.gradle') diff --git a/grails-core/src/test-cli/groovy/org/apache/grails/core/cli/io/FileSystemInteractionImplSpec.groovy b/grails-core/src/test-cli/groovy/org/apache/grails/core/cli/io/FileSystemInteractionImplSpec.groovy index c217b9436cc..5e58df9e1a8 100644 --- a/grails-core/src/test-cli/groovy/org/apache/grails/core/cli/io/FileSystemInteractionImplSpec.groovy +++ b/grails-core/src/test-cli/groovy/org/apache/grails/core/cli/io/FileSystemInteractionImplSpec.groovy @@ -66,7 +66,7 @@ class FileSystemInteractionImplSpec extends Specification { } then: - interaction.projectPath(missing) == 'nested/missing.txt' + interaction.projectPath(missing) == new File('nested', 'missing.txt').path interaction.projectPath('missing.txt') == 'missing.txt' !interaction.resource(missing).exists() pendingInteraction.file('build.gradle') == new File(pending, 'build.gradle') diff --git a/grails-shell-cli/src/test/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImplSpec.groovy b/grails-shell-cli/src/test/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImplSpec.groovy index c7acbb161df..ff750fc1221 100644 --- a/grails-shell-cli/src/test/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImplSpec.groovy +++ b/grails-shell-cli/src/test/groovy/org/grails/cli/profile/commands/io/FileSystemInteractionImplSpec.groovy @@ -80,7 +80,7 @@ class FileSystemInteractionImplSpec extends Specification { } then: - interaction.projectPath(missing) == 'nested/missing.txt' + interaction.projectPath(missing) == new File('nested', 'missing.txt').path interaction.projectPath('missing.txt') == 'missing.txt' !interaction.resource(missing).exists() pendingInteraction.file('build.gradle') == new File(pending, 'build.gradle') From 07255010d40c9c2464aa48ae09ff1e5cecfc8f8e Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Sun, 18 Oct 2026 07:57:35 -0400 Subject: [PATCH 12/14] Set the 10.0.x branch version to 10.0.0-SNAPSHOT and githubBranch to 10.0.x 10.0.x was branched from 9.1.x without the version bump, so gradle.properties still said projectVersion=9.1.0-SNAPSHOT and githubBranch=9.1.x. The 'Validate githubBranch' job therefore fails on every pull request whose base is 10.0.x. Same four files as the 9.1.x bump in 829de6092e. Co-Authored-By: Claude Fable 5.1 (cherry picked from commit fb8ecf06f84a20e9b90f7f5e37c03ed292614257) --- THREAT_MODEL.md | 2 +- gradle.properties | 8 ++++---- .../src/test/groovy/grails/util/GrailsUtilTests.java | 2 +- threat-model.yaml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/THREAT_MODEL.md b/THREAT_MODEL.md index 9b2abe1b66d..84a6613bc20 100644 --- a/THREAT_MODEL.md +++ b/THREAT_MODEL.md @@ -19,7 +19,7 @@ limitations under the License. ## §1 Header - **Project**: Apache Grails (`apache/grails-core`) -- **Version binding**: 9.1.x branch. A report against version *N* is triaged against this document as it stood at *N*, not at HEAD. +- **Version binding**: 10.0.x branch. A report against version *N* is triaged against this document as it stood at *N*, not at HEAD. - **Date**: 2026-01 - **Author**: Apache Grails PMC and contributors (initial draft). - **Status**: **DRAFT** - not yet ratified by maintainers. Open questions in §14 must be resolved before this document is binding. diff --git a/gradle.properties b/gradle.properties index 88efad23a6e..a1530bfd963 100644 --- a/gradle.properties +++ b/gradle.properties @@ -13,20 +13,20 @@ # See the License for the specific language governing permissions and # limitations under the License. -projectVersion=9.1.0-SNAPSHOT +projectVersion=10.0.0-SNAPSHOT javaVersion=21 -githubBranch=9.1.x +githubBranch=10.0.x # Last day this release line is supported (ISO yyyy-MM-dd). Grails Forge reports it from /versions. # Each release branch sets its own value. grailsEndOfSupport=2027-12-31 githubSlug=apache/grails-core -# 9.1.0-SNAPSHOT has never been published, so the Spring Dependency Management +# 10.0.0-SNAPSHOT has never been published, so the Spring Dependency Management # example cannot resolve its grails-bom import yet. Delete this line once the first -# 9.1.0-SNAPSHOT publish succeeds - the flag is presence-based, so setting it to +# 10.0.0-SNAPSHOT publish succeeds - the flag is presence-based, so setting it to # false still excludes the example. See settings.gradle for the full explanation. bomSnapshotNotPublished=true diff --git a/grails-core/src/test/groovy/grails/util/GrailsUtilTests.java b/grails-core/src/test/groovy/grails/util/GrailsUtilTests.java index c78f04589e2..181be4e1f9d 100644 --- a/grails-core/src/test/groovy/grails/util/GrailsUtilTests.java +++ b/grails-core/src/test/groovy/grails/util/GrailsUtilTests.java @@ -33,7 +33,7 @@ public class GrailsUtilTests { @Test public void testGrailsVersion() { - assertEquals("9.1.0-SNAPSHOT", GrailsUtil.getGrailsVersion()); + assertEquals("10.0.0-SNAPSHOT", GrailsUtil.getGrailsVersion()); } @AfterEach diff --git a/threat-model.yaml b/threat-model.yaml index fbce496d182..b05051604ad 100644 --- a/threat-model.yaml +++ b/threat-model.yaml @@ -21,7 +21,7 @@ project: name: apache/grails-core - version_binding: "9.1.x" + version_binding: "10.0.x" threat_model: THREAT_MODEL.md status: draft date: "2026-01" From bb2a057a67e7030866929f35f2e5dcb6caa4e097 Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Mon, 19 Oct 2026 16:16:51 -0500 Subject: [PATCH 13/14] CHANGEME FOR ACCEPTANCE: skip tests and code style on stacked PRs (remove before merge) Keeps CI on the stacked Java-to-Groovy PRs cheap while the chain is reviewed and rebased. The PR closest to the release branch keeps the full build; this block is dropped from a PR when its base is merged. Merging with this commit is a blocker. Co-Authored-By: Claude Sonnet 5.5 --- gradle.properties | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/gradle.properties b/gradle.properties index a1530bfd963..044822d4a43 100644 --- a/gradle.properties +++ b/gradle.properties @@ -138,3 +138,9 @@ org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx3G # property, hence the systemProp prefix. gradle/dependency-analysis.settings.gradle holds the # matching concurrency limit. systemProp.dependency.analysis.cache.max=200 + +# CHANGEME FOR ACCEPTANCE: remove this block before merging. It keeps CI on the stacked Java-to-Groovy PRs +# cheap (compile only, no tests, no code style) so branches can be rebased and force-pushed freely. Only the +# PR closest to its release branch is built in full. Merging with these lines is a BLOCKER. +skipTests=true +skipCodeStyle=true From 52a657670135255c63cec2581a7e945d4636c13e Mon Sep 17 00:00:00 2001 From: Walter Duque de Estrada Date: Mon, 19 Oct 2026 19:36:36 -0500 Subject: [PATCH 14/14] CHANGEME FOR ACCEPTANCE: disable PR workflow triggers on stacked PRs (revert before merge) The skipTests/skipCodeStyle flag only removed test execution; the functional jobs still compiled the whole repository (~57 minutes each) on every push. Turn the pull_request triggers into a branch filter that never matches so pushes, rebases and force-pushes to the stacked PRs start no jobs. Reverts the gradle.properties flag. The PR closest to the release branch keeps its triggers. Merging with this commit is a blocker. Co-Authored-By: Claude Sonnet 5.5 --- .github/workflows/benchmark.yml | 7 ++----- .github/workflows/build-health.yml | 2 ++ .github/workflows/codeanalysis.yml | 2 ++ .github/workflows/codeql.yml | 6 ++---- .github/workflows/codestyle.yml | 2 ++ .github/workflows/coverage.yml | 2 ++ .github/workflows/end-to-end.yml | 2 ++ .github/workflows/gradle.yml | 2 ++ .github/workflows/groovy-snapshot-canary.yml | 4 ++-- .github/workflows/rat.yml | 5 ++--- .github/workflows/sitemesh2.yml | 8 ++------ .github/workflows/validate-actions.yml | 6 ++---- .github/workflows/vulnerability-scan.yml | 3 ++- gradle.properties | 6 ------ 14 files changed, 26 insertions(+), 31 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 53e88c886ab..dd1e1c37c77 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -20,11 +20,8 @@ name: "JMH Benchmark Comparison" # to that code through a privileged pull_request_target workflow. on: pull_request: - types: [opened, synchronize, reopened, labeled] - paths-ignore: - - '**/*.md' - - '**/*.adoc' - - 'grails-doc/**' + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }}-${{ github.event.action == 'labeled' && github.event.label.name != 'performance' && github.run_id || 'benchmark' }} diff --git a/.github/workflows/build-health.yml b/.github/workflows/build-health.yml index a8564b0c350..fa303225bac 100644 --- a/.github/workflows/build-health.yml +++ b/.github/workflows/build-health.yml @@ -19,6 +19,8 @@ on: branches: - '[0-9]+.[0-9]+.x' pull_request: + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] workflow_dispatch: # queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits concurrency: diff --git a/.github/workflows/codeanalysis.yml b/.github/workflows/codeanalysis.yml index d370384dd71..cb560cd9b37 100644 --- a/.github/workflows/codeanalysis.yml +++ b/.github/workflows/codeanalysis.yml @@ -20,6 +20,8 @@ on: - '[0-9]+.[0-9]+.x' - '8.0.x-hibernate7.*' pull_request: + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] workflow_dispatch: # queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits concurrency: diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index d72fca1f377..6459eebb07e 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -32,10 +32,8 @@ on: - '[4-9]+.[0-9]+.x' - '8.0.x-hibernate7.*' pull_request: - # The branches below must be a subset of the branches above - branches: - - '[4-9]+.[0-9]+.x' - - '8.0.x-hibernate7.*' + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] # queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/codestyle.yml b/.github/workflows/codestyle.yml index d5e14ee8ade..5af095573fe 100644 --- a/.github/workflows/codestyle.yml +++ b/.github/workflows/codestyle.yml @@ -20,6 +20,8 @@ on: - '[0-9]+.[0-9]+.x' - '8.0.x-hibernate7.*' pull_request: + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] workflow_dispatch: # queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits concurrency: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index bcfb02719f5..efe587199c0 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -21,6 +21,8 @@ on: - '[0-9]+.[0-9]+.x' - '8.0.x-hibernate7.*' pull_request: + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] workflow_dispatch: concurrency: diff --git a/.github/workflows/end-to-end.yml b/.github/workflows/end-to-end.yml index 3986680e065..80917897cd1 100644 --- a/.github/workflows/end-to-end.yml +++ b/.github/workflows/end-to-end.yml @@ -38,6 +38,8 @@ on: branches: - '[0-9]+.[0-9]+.x' pull_request: + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 82692baefe8..0725433ae8b 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -19,6 +19,8 @@ on: branches: - '[0-9]+.[0-9]+.x' pull_request: + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] workflow_dispatch: # Queue jobs - cancel in-progress PR runs when new commits pushed, but allow branch builds to complete concurrency: diff --git a/.github/workflows/groovy-snapshot-canary.yml b/.github/workflows/groovy-snapshot-canary.yml index c795d9db261..dd97156a715 100644 --- a/.github/workflows/groovy-snapshot-canary.yml +++ b/.github/workflows/groovy-snapshot-canary.yml @@ -19,8 +19,8 @@ on: branches: - '[0-9]+.[0-9]+.x' pull_request: - branches: - - '[0-9]+.[0-9]+.x' + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] workflow_dispatch: # queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits concurrency: diff --git a/.github/workflows/rat.yml b/.github/workflows/rat.yml index f13a580f683..42d724ce9ae 100644 --- a/.github/workflows/rat.yml +++ b/.github/workflows/rat.yml @@ -20,9 +20,8 @@ on: - '[0-9]+.[0-9]+.x' - '8.0.x-hibernate7.*' pull_request: - branches: - - '[0-9]+.[0-9]+.x' - - '8.0.x-hibernate7.*' + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] workflow_dispatch: # queue jobs and only allow 1 run per branch due to the likelihood of hitting GitHub resource limits concurrency: diff --git a/.github/workflows/sitemesh2.yml b/.github/workflows/sitemesh2.yml index 2c9b81232d3..e4469b4f6b5 100644 --- a/.github/workflows/sitemesh2.yml +++ b/.github/workflows/sitemesh2.yml @@ -34,12 +34,8 @@ on: - 'grails-dependencies/**' - '.github/workflows/sitemesh2.yml' pull_request: - paths: - - 'grails-gsp/**' - - 'grails-test-suite-uber/**' - - 'grails-test-examples/**' - - 'grails-dependencies/**' - - '.github/workflows/sitemesh2.yml' + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] # Queue jobs - cancel in-progress PR runs when new commits pushed, but allow branch builds to complete concurrency: group: ${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/validate-actions.yml b/.github/workflows/validate-actions.yml index eaad416c080..70fe0b5e615 100644 --- a/.github/workflows/validate-actions.yml +++ b/.github/workflows/validate-actions.yml @@ -24,10 +24,8 @@ on: - 'build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/ValidateActionsTask.groovy' - 'build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsValidateActionsPlugin.groovy' pull_request: - paths: - - '.github/workflows/**' - - 'build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/ValidateActionsTask.groovy' - - 'build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/GrailsValidateActionsPlugin.groovy' + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] schedule: - cron: '0 6 * * 1' # weekly Monday 06:00 UTC — catches ASF delisting a pinned SHA workflow_dispatch: diff --git a/.github/workflows/vulnerability-scan.yml b/.github/workflows/vulnerability-scan.yml index 147775b9327..9824ebb7144 100644 --- a/.github/workflows/vulnerability-scan.yml +++ b/.github/workflows/vulnerability-scan.yml @@ -22,7 +22,8 @@ on: # Opt in per pull request by applying the "vulnerability scan" label. Applying a label # requires write access, so only a committer can start a scan. pull_request: - types: [labeled, synchronize, reopened] + # CHANGEME FOR ACCEPTANCE: PR triggers disabled on this stacked branch; revert before merge + branches: ['CHANGEME-FOR-ACCEPTANCE'] concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} diff --git a/gradle.properties b/gradle.properties index 044822d4a43..a1530bfd963 100644 --- a/gradle.properties +++ b/gradle.properties @@ -138,9 +138,3 @@ org.gradle.jvmargs=-Dfile.encoding=UTF-8 -Xmx3G # property, hence the systemProp prefix. gradle/dependency-analysis.settings.gradle holds the # matching concurrency limit. systemProp.dependency.analysis.cache.max=200 - -# CHANGEME FOR ACCEPTANCE: remove this block before merging. It keeps CI on the stacked Java-to-Groovy PRs -# cheap (compile only, no tests, no code style) so branches can be rebased and force-pushed freely. Only the -# PR closest to its release branch is built in full. Merging with these lines is a BLOCKER. -skipTests=true -skipCodeStyle=true