package javax0.geci.tools; import javax0.geci.annotations.Geci; import javax0.geci.annotations.Generated; import javax0.geci.api.GeciException; import javax0.geci.api.Source; import javax0.geci.tools.reflection.Selector; import java.lang.annotation.Annotation; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Arrays; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Stream; public class GeciAnnotationTools { private static final Pattern SEGMENT_HEADER_PATTERN = Pattern.compile("//\\s*<\\s*editor-fold(.*)>"); private static final Pattern ANNOTATION_PATTERN = Pattern.compile("@Geci\\(\"(.*)\"\\)"); private static final Pattern pattern = Pattern.compile("([\\w\\d_$]+)\\s*=\\s*'(.*?)'"); /** * Get the strings of the values of the {@link Geci} annotations * that are on the element parameter. The {@link Geci} annotation * has a single value parameter that is a string. * * <p> * The method takes care of the special case when there is only one * {@link Geci} annotation on the element and also when there are * many. * * <p> * Note that the annotation does not need to be the one, which is * defined in the javageci annotation library. It can be any * annotation interface so long as long the name is {@code Geci} and * the method {@code value()} returns {@code java.lang.String}. * * @param element the class, method or field that is annotated. * @return the array of strings that contains the values of the * annotations. If the element is not annotated then the returned * array will have zero elements. If there is one {@link Geci} * annotation then the returned String array will have one element. * If there are many annotations then the array will contains each * of the values. */ public static String[] getGecis(AnnotatedElement element) { return getDeclaredAnnotationUnwrapped(element) .filter(GeciAnnotationTools::isAnnotationGeci) .map(GeciAnnotationTools::getValue) .toArray(String[]::new); } /** * @param element the annotated element for which we need the * annotations * @return a stream of the annotations used on the {@code element}. * In case any of the annotations is a collection of repeated * annotations then the repeated annotations will be returned * instead of the collecting annotations. I.e.: if an element has a * {@code Gecis} annotation, which is never used directly in the * source code, but is put into the byte code by the compiler to * wrap the repeated {@code Geci} annotations then the stream will * contain the {@code Geci} annotations and not the one {@code * Gecis}. */ private static Stream<Annotation> getDeclaredAnnotationUnwrapped(AnnotatedElement element) { final var allAnnotations = element.getDeclaredAnnotations(); return Arrays.stream(allAnnotations) .flatMap(GeciAnnotationTools::getSelfOrRepeated); } /** * Get a stream that delivers the annotation itself, or a stream * that returns the annotations that are repeated using this * annotation as a group annotations. * * <p>For example the module {@code annotations} in Java::Geci * defines the annotations {@code Geci} and also {@code Gecis}. * Source code will never directly use {@code Gecis}, but they may * contain several {@code Geci} annotations. * * <p>When a class or other annotated element has several {@code * Geci} annotations then these annotations are collected into a * {@code Gecis} annotation and this one is returned. The {@code * value()} method of this annotation returns an array of {@code * Geci} annotations. This is standard Java annotation handling. * * <p>This method returns a stream. When the annotation is a * "normal" annotation then the stream will contain only one * element. When the annotation is used to group several annotations * and their {@code value()} method returns an array of annotation * objects then the stream will deliver these objects instead of the * annotation. * * @param annotation which will be delivered in the stream or which * contains the other annotations. * @return the stream of the annotation or annotations */ private static Stream<Annotation> getSelfOrRepeated(Annotation annotation) { try { final var value = annotation.annotationType().getMethod("value"); value.setAccessible(true); if (Annotation[].class.isAssignableFrom(value.getReturnType())) { return Stream.of((Annotation[]) value.invoke(annotation)); } else { return Stream.of(annotation); } } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { return Stream.of(annotation); } } /** * Checks that an annotation is a Geci annotation or not. * <p> * <p> * An annotation is Geci annotation in case the name of the * annotation interface is {@code Geci} or if the annotation * interface itself is annotated with a Geci annotation. * <p> * <p> * This is a recursive definition and because annotations may be * annotated recursively directly by themselves or indirectly * through other annotations we have to be careful not to check an * annotation for "Geciness" that we have already started to check. * * <p> * <p> * The rule is that if an annotation could only be Geci because it * is recursively annotated by itself then it is not Geci. Somewhere * in the loop there has to be an annotation that has the name * {@code Geci}. * * @param annotation the annotation that we want to know if it is * Geci or not * @return {@code true} if the annotation is a Geci annotation. */ private static boolean isAnnotationGeci(Annotation annotation) { return isAnnotationGeci(annotation, new HashSet<>()); } /** * This is the recursive implementation of {@link * #getGecis(AnnotatedElement)}. The second parameter is an empty * set at the start and later it is filled when invoked recursively. * This will prevent infinite loops in case there is an annotation * loop, like {@code @interface A} if annotated using {@code @B} and * {@code @interface B} is annotared using {@code @A}. * * @param annotation the annotation that we want to know if it is * Geci or not * @param checked a set containing all the annotations for which * this method was already invoked. (Not * necessarily returned yet though.) * @return {@code true} if the annotation is a Geci annotation. */ private static boolean isAnnotationGeci(Annotation annotation, Set<Annotation> checked) { checked.add(annotation); if (annotationName(annotation).equals("Geci")) { return true; } final var annotations = annotation.annotationType() .getDeclaredAnnotations(); return Arrays.stream(annotations) .filter(x -> !checked.contains(x)) .anyMatch(x -> isAnnotationGeci(x, checked)); } private static String annotationName(Annotation a) { return a.annotationType().getSimpleName(); } /** * Get the value string from the annotation and in case there are * other annotation parameters that return a {@code String} value * and they are defined on the annotation then append the * "key='value'" after the value string. That way the annotation * parameters become part of the configuration. * * <p>Also when the value does not contain a mnemonic at the start * then the name of the annotation will be prepended to the string * with a space separating it from the parameters. Note that since * annotations are Java interfaces and thus are supposed to start * with upper case letters but mnemonics are like variables, * starting with lower case letters the name of the annotation is * modified lower casing the first character. * * @param annotation the annotation that contains the configuration * @return the configuration string */ static String getValue(Annotation annotation) { try { final String rawValue = getRawValue(annotation); final var annotationName = getAnnotationGeciName(annotation); final var value = getValue(CaseTools.lcase(annotationName), rawValue.trim()); for (final var method : getAnnotationDeclaredMethods(annotation)) { if (method.getReturnType().equals(String.class) && !method.getName().equals("value") && !method.getName().equals("toString")) { final String param = geParam(annotation, method); if (param != null && param.length() > 0) { value.append(" ") .append(method.getName()) .append("='") .append(param) .append("'"); } } } return value.toString(); } catch (ClassCastException e) { throw new GeciException("Cannot use '" + annotationName(annotation) + "' as generator annotation.", e); } } private static String getAnnotationGeciName(Annotation annotation) { final var selfName = annotation.annotationType().getSimpleName(); final var annotations = annotation.annotationType() .getDeclaredAnnotations(); final var renamedName = Arrays.stream(annotations) .filter(GeciAnnotationTools::isAnnotationGeci).map(GeciAnnotationTools::getRawValue).findFirst(); return renamedName.isPresent() && renamedName.get().length() > 0 ? renamedName.get() : selfName; } private static String geParam(Annotation annotation, Method method) { try { method.setAccessible(true); return (String) method.invoke(annotation); } catch (IllegalAccessException | InvocationTargetException e) { return ""; } } private static String getRawValue(Annotation annotation) { try { final var valueMethod = getAnnotationValueMethod(annotation); valueMethod.setAccessible(true); final var value = valueMethod.invoke(annotation); if (value instanceof String) { return (String) value; } if (value instanceof String[]) { final var values = (String[]) value; return String.join(" ",values); } throw new IllegalArgumentException("The annotation " + annotationName(annotation) + " value() return type is not String or String[]"); } catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) { return ""; } } /** * Get the value method of an annotation. * * <p>Implementation notes: when executing this code in an * application that uses JPMS (e.g. Java::Geci) then the annotation * object itself is not an instance of the annotation interface. It * is fairly obvious, since interfaces do not have instances. The * object is the instance of a proxy class which is in the package * {@code com.sun.proxy.jdk.proxy1} in the module {@code * jdk.proxy1}. This module does not "opens" this package and there * is no way to open this package for good reason. Because it is not * opened for reflective access we cannot invoke any method of this * class on the annotation object instance, let alone we cannot even * call {@code setAccessible()} on it. * * <p>The good news is that we do not need. What we really need is * to call the method of the interface reflectively on the * annotation object instance. * * <p>In short: we invoke the interface method and not the * class method on the object. * * <p>Annotation objects have only one interface they implement and * that is the annotation interface. Therefore there is no need to * check that the annotation class has interfaces and how many. We * just grab the annotation interface as the zero-th element of the * array. * * @param annotation of which we need the value method * @return the value method * @throws NoSuchMethodException when the annotation does not have a * {@code value()} method */ private static Method getAnnotationValueMethod(Annotation annotation) throws NoSuchMethodException { return annotation.annotationType().getDeclaredMethod("value"); } /** * Get the declared methods of the annotation that this annotation * object instance implements. See also the implementation comments * on the documentation of {@link * #getAnnotationValueMethod(Annotation)}. * * @param annotation of which we need the declared methods * @return the array of declared methods */ private static Method[] getAnnotationDeclaredMethods(Annotation annotation) { return annotation.annotationType().getDeclaredMethods(); } /** * Get the value string adjusted with the name of the annotation. * * <p> The annotation value string should start with the mnemonic of * the generator. If the generator uses it's own annotation than the * name of the annotation can be used to match the mnemonic of the * generator. For example, if there is a generator that has the * mnemonic {@code mygenerator} then the annotation {@code * MyGenerator} can be used to configure it. At the same time it * would be waste of characters to write * * <pre>{@code * * @MyGenerator("mygenerator a='1' b='2' ... z='xxx"") * * }</pre> <p> Therefore in situations like this the mnemonic can be * omitted from the start of the configuration string and Java::Geci * will use the name of the annotation first character lower cased * as the mnemonic of the generator. Thus * * <pre>{@code * * @MyGenerator("a='1' b='2' ... z='xxx"") * * }</pre> * <p> * will be served to the generator {@code mygenerator} even though * the configuration string in the annotation does not start with * this mnemonic of the generator. * * <p> * This method checks the value string and if it starts with a * mnemonic then it simply returns the string. If there seems to be * a mnemonic missing from the start of the string then it will * prepend the name of the class of the annotation in from of the * string separated by a space. * * <p> * If the string that starts at the start of the value string and * lasts till the first space of at last the end of it contains a * {@code =} character then it is not a mnemonic and then the * mnemonic will be inserted. Otherwise the value will be return * virgo intacta. * * @param annotationName the name of the annotation to be used as * mnemonic (first character already * lowercase) * @param value the value to modify or leave alone * @return the value modified or as it was */ private static StringBuilder getValue(String annotationName, String value) { final var mnemonicEnd = value.indexOf(' '); final String mnemonic = mnemonicEnd == -1 ? value : value.substring(0, mnemonicEnd); if (mnemonic.contains("=") || mnemonic.length() == 0) { return new StringBuilder(annotationName) .append(value.length() == 0 ? "" : " ") .append(value); } else { return new StringBuilder(value); } } /** * Checks if the element is real source code or was generated. * * <p> Generators are encouraged to annotate the generated elements * with the annotation {@link Generated}. This is good for the human * reader and the same time some generators can decide if an element * is in the compiled class because it was generated or because the * programmer provided a version for the element manually. For * example the delegator generator does not generate the delegating * methods that are provided by the programmer manually but it * regenerates all methods that are needed and have the {@link * Generated} annotation. * * @param element that needs the decision if it is generated or * manually programmed * @return {@code true} if the element was generated (has the * annotation {@link Generated}). */ public static boolean isGenerated(AnnotatedElement element) { return Selector.compile("annotation ~ /Generated/").match(element); } /** * Get the parameters from the source file directly reading the * source. When a generator uses this method the project may not * need {@code com.javax0.geci:javageci-annotation:*} as a {@code * compile} time dependency when the "annotation" is commented out. * This configuration tool can also be used when the source is not * Java, as it does not depend on Java annotations. * * <p> The lines of the source are read from the start and the * parameters composed from the first line that is successfully * processed are returned. * * @param source the source object holding the code lines * @param generatorMnemonic the name of the generator that needs the * parameters. Only the parameters that are * specific for this generator are read. * @param prefix characters that should prefix the * annotation. In case of Java it is {@code * //}. The line is first trimmed from * leading and trailing space, then the * {@code prefix} characters are removed * from the start then it has to match the * annotation syntax. If this parameter is * {@code null} then it is treated as empty * string, a.k.a. no prefix. * @param nextLine is a regular expression that should * match the line after the successfully * matched configuration line. If the next * line does not match the pattern then the * previous line is ignored. Typically this * is something line {@code * /final\s*int\s*xx} when the generator * wants to get the parameters for the * {@code final int xx} declaration. If * this variable is {@code null} then there * is no pattern matching performed, and * all parameter holding line that looks * like a {@code Geci} annotation is * accepted and processed. * <p> Note also that if one or more lines * looks like {@code Geci} annotations then * they are skipped and the {@code * nextLine} pattern is matched against the * next line that is not a configuration * line. This allows the program to have * multiple configuration lines for * different generators preceding the same * source line. * @return the new {@link CompoundParams} object or {@code null} in * case there is no configuration found in the file for the * specific generator with the specified conditions. */ public static CompoundParams getParameters(Source source, String generatorMnemonic, String prefix, Pattern nextLine) { CompoundParams paramConditional = null; for (var line : source.getLines()) { if (paramConditional != null) { if (nextLine == null || nextLine.matcher(line).find()) { return paramConditional; } } final Matcher match = getMatch(prefix, line); if (match.matches()) { if (paramConditional == null) { var string = match.group(1); paramConditional = getParameters(generatorMnemonic, string); } } else { paramConditional = null; } } return null; } /** * Get the parameters from the source file directly reading the * source. This method tries to find a line that has the format * * <pre>{@code * // <editor-fold id="mnemonic" a="parm" b="param" ... > * }</pre> * * <p> and read the parameters from that line. * * <p>The parameter {@code desc} is ignored since that is used by * the editor to display a short description when the editor fold is * closed and thus it would not be nice to forbid the use of it in * case the generator does not have a parameter named "desc". * * @param source the source object holding the code lines * @param mnemonic the name of the generator * @return a compound object that contains the parameters defined in * the segments that have the {@code id="mnemonic"} or {@code null} * if there is no appropriate segment starting line that would match * the syntax and the mnemonic * @deprecated The parameters of a segment should be accessed directly * through the Segment object representing it. */ @Deprecated(/*since = "1.2.0"*/) public static CompoundParams getSegmentParameters(Source source, String mnemonic) { for (var line : source.getLines()) { final var trimmedLine = line.trim(); final var headerMatcher = SEGMENT_HEADER_PATTERN.matcher(trimmedLine); if (headerMatcher.matches()) { final var params = new CompoundParamsBuilder(headerMatcher.group(1)).exclude("desc").redefineId().build(); if (mnemonic.equals(params.id())) { return params; } } } return null; } public static CompoundParams getParameters(String generatorMnemonic, String string) { if (string.startsWith(generatorMnemonic + " ") || string.equals(generatorMnemonic)) { return new CompoundParamsBuilder(string).redefineId().build(); } else { return null; } } /** * Get a matcher of the line against the {@code @Geci( ... ) } * pattern to extract the configuration parameters from a comment * line. Before the regular expression matching the line is trimmed, * prefix is chopped off from the start and the end of * the line and then the remaining line is trimmed again. * * @param prefix the string that is chopped off from the start of the line if it is there * @param line the line to match * @return the matcher of regular expression matching */ private static Matcher getMatch(String prefix, String line) { final var trimmedLine = line.trim(); final var chopped = prefix != null && trimmedLine.startsWith(prefix) ? trimmedLine.substring(prefix.length()) : trimmedLine; final var matchLine = chopped.trim(); return ANNOTATION_PATTERN.matcher(matchLine); } /** * This method is not used any more. The functionality was moved to * {@link CompoundParamsBuilder} and it is based on lexical analysis * instead of simple regular expression use. * * <p> Get the parameters into a map from the string. The {@link * Geci} annotation has one single value that is a string. This * string is supposed to have the format: * * <pre> * * generator_menomic key='value' ... key='value' * </pre> * * <p> The key can be anything that is more or less an identifier * (contains only alphanumeric characters, underscore and {@code $} * character, but can also start with any of those, thus it could be * '{@code 1966}'). * * <p>The value is enclosed between single quotes, that makes it * easier to type and read as single quotes do not need escaping in * strings. These quotes can not be skipped. * * @param s the string parameter * @return the map composed from the string */ @Deprecated(/*forRemoval = true*/) public static Map<String, String> getParameters(String s) { var pars = new HashMap<String, String>(); var matcher = pattern.matcher(s); while (matcher.find()) { var key = matcher.group(1); var value = matcher.group(2); pars.put(key, value); } return pars; } }