diff --git a/LICENSE b/LICENSE index 5ba0a90..9493604 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2021 Mohamed Ashraf Bayor +Copyright (c) 2022 Mohamed Ashraf Bayor Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 44cbb71..de20e29 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,9 @@ -## JISEL: Java Interface Segregation Library -Interface Segregation Library for Java 17 +# JISEL: Java Interface Segregation Library +Minimum Java 17 Required -Pitch Video: -... +Pitch Video: https://youtu.be/nkbu6zxV3R0 -### How to Install ? +## Installation If you are running a Maven project, add the latest release dependency to your pom.xml ```xml @@ -14,35 +13,98 @@ If you are running a Maven project, add the latest release dependency to your po 1.0 ``` +You will also need to include the same dependency as an additional annotation processor in the Maven Compiler plugin of your project +```xml + + + + org.apache.maven.plugins + maven-compiler-plugin + ${maven-compiler-plugin.version} + + 17 + -Xlint:unchecked + + + org.jisel + jisel + 1.0 + + + + + + +``` + For other build tools, please check: [Maven Central](https://search.maven.org/artifact/org.jisel/jisel/1.0/jar). -### Use on your declared bloated interfaces methods +## Provided Annotations +### @SealForProfile / @SealForProfiles +To be used only on abstract methods of the large interfaces you need to segregate ```java -import SealForProfile; +public interface Sociable { + + String STUDENT = "Student"; + String WORKER = "Worker"; + String ACTIVE_WORKER = "ActiveWorker"; + + @SealForProfiles({STUDENT, WORKER, ACTIVE_WORKER}) + String startConversation(); + + @SealForProfile(STUDENT) + boolean attendClass(String fieldOfStudy); + + @SealForProfile(STUDENT) + void askForHelpWhenNeeded(); + + @SealForProfile(WORKER) + @SealForProfile(ACTIVE_WORKER) + // both annotations above can be replaced with: @SealForProfiles({WORKER, ACTIVE_WORKER}) + boolean[] joinOfficeSocialGroups(String[] groups, int maximum); -public interface InterfaceA { - @SealForProfile - void something(); + @SealForProfile(ACTIVE_WORKER) + void leadOfficeSocialGroup(String groupName); + + @SealForProfile(ACTIVE_WORKER) + double createNewOfficeSocialGroup(String groupName, List starters); } ``` -### Use on your declared child classes implementing generated sealed interfaces - +### @AddToProfile / @AddToProfiles +To be used only on classes (or interfaces) implementing (or extending) any of the sealed interfaces generated by the use of @SealForProfile(s) ```java -import AddToProfile; +@AddToProfiles(profiles = {STUDENT, WORKER}, largeInterface = "com.bayor.jisel.annotation.client.hierarchicalinheritance.Sociable") +// the above line can be replaced with the following 2: +// @AddToProfile(profile = STUDENT, largeInterface = "com.bayor.jisel.annotation.client.hierarchicalinheritance.Sociable") +// @AddToProfile(profile = WORKER, largeInterface = "com.bayor.jisel.annotation.client.hierarchicalinheritance.Sociable") +public final class StudentWorkerHybrid implements SealedStudentSociable, SealedWorkerSociable { + @Override + public String startConversation() throws IllegalStateException { + return null; + } + + @Override + public void askForHelpWhenNeeded() { + + } -@AddToProfile("PROFILE_NAME") -public final class ClientA implements SealedInterfaceA { - // ... + @Override + public boolean attendClass(String param0) throws IllegalArgumentException { + return false; + } + + @Override + public boolean[] joinOfficeSocialGroups(String[] param0, int param1) { + return new boolean[0]; + } } + ``` -### Sample classes for testing +### Sample interfaces and classes for testing [https://github.com/mohamed-ashraf-bayor/jisel-annotation-client](https://github.com/mohamed-ashraf-bayor/jisel-annotation-client) -### Invalid Uses of Jisel -The annotation should be used ONLY on interfaces created in your own project. - ### Issues, Bugs, Suggestions Contribute to the project's growth by reporting issues or making improvement suggestions [here](https://github.com/mohamed-ashraf-bayor/jisel/issues/new/choose) diff --git a/pom.xml b/pom.xml index 4959b0c..e8cfd51 100644 --- a/pom.xml +++ b/pom.xml @@ -9,8 +9,8 @@ jar Jisel - Interface Segregation Library for Java 17 - http://jisel.org + Java Interface Segregation Library + https://jisel.org/ @@ -21,7 +21,7 @@ - scm:git:git://github.com/mohamed-ashraf-bayor/froporec.git + scm:git:git://github.com/mohamed-ashraf-bayor/jisel.git scm:git:git@github.com:mohamed-ashraf-bayor/mohamed-ashraf-bayor.git https://github.com/mohamed-ashraf-bayor/jisel HEAD @@ -52,7 +52,7 @@ com.google.auto.service - auto-service + auto-service-annotations ${auto-service.version} @@ -66,6 +66,13 @@ 17 -Xlint:unchecked + + + com.google.auto.service + auto-service + ${auto-service.version} + + diff --git a/src/main/java/module-info.java b/src/main/java/module-info.java new file mode 100644 index 0000000..9e74b14 --- /dev/null +++ b/src/main/java/module-info.java @@ -0,0 +1,27 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +module jisel { + exports org.jisel; + requires java.compiler; + requires java.logging; + requires com.google.auto.service; +} \ No newline at end of file diff --git a/src/main/java/org/jisel/AddToProfile.java b/src/main/java/org/jisel/AddToProfile.java index 81bd070..db8d434 100644 --- a/src/main/java/org/jisel/AddToProfile.java +++ b/src/main/java/org/jisel/AddToProfile.java @@ -1,3 +1,24 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package org.jisel; import java.lang.annotation.ElementType; @@ -6,16 +27,48 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import static org.jisel.generator.StringGenerator.EMPTY_STRING; + +/** + * Annotation to be applied on top of a class, an interface or a record which is implementing or extending a sealed interface generated by Jisel.

+ * All sealed interfaces generated by Jisel follow the naming convention: Sealed<ProfileName><LargeInterfaceSimpleName>
+ * See @{@link AddToProfile} attributes documentation.

+ */ @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE}) -@Repeatable(AddToProfile.AddToProfiless.class) +@Repeatable(AddToProfile.AddToProfilez.class) public @interface AddToProfile { - String value(); + /** + * Not Required - specifies the name of one of the profiles used with the @SealForProfile annotations in the large interface definition. + * Also corresponds to the <ProfileName> from the sealed interfaces naming convention.
+ * If not provided or empty, the annotated class, interface or record will be added to the permits list of the generated parent sealed interface.
+ * Also, the provided profile attribute value MUST be one of the profiles defined in the large interface definition using @SealForProfile. If not, + * the specified profile will be ignored and an informational message regarding provided incorrect profiles will be printed during the compilation.
+ * + * @return the profile name + */ + String profile() default EMPTY_STRING; + + /** + * Required - MUST be the Fully Qualified Name of the large interface. That would be the <LargeInterfaceSimpleName> as seen in + * the sealed interface name convention, preceded by the package name.
+ * + * @return the large interface fully qualified name + */ + String largeInterface(); + /** + * Internal annotation allowing @AddToProfile to be repeatable + */ @Retention(RetentionPolicy.SOURCE) @Target(ElementType.TYPE) - @interface AddToProfiless { + @interface AddToProfilez { + /** + * array attribute allowing @{@link AddToProfile} to be repeatable + * + * @return array of @AddToProfile instances + */ AddToProfile[] value(); } } \ No newline at end of file diff --git a/src/main/java/org/jisel/AddToProfiles.java b/src/main/java/org/jisel/AddToProfiles.java index c89af8b..4a7d201 100644 --- a/src/main/java/org/jisel/AddToProfiles.java +++ b/src/main/java/org/jisel/AddToProfiles.java @@ -1,12 +1,74 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package org.jisel; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import static org.jisel.generator.StringGenerator.EMPTY_STRING; + +/** + * Annotation to be applied on top of a class, an interface or a record which is implementing or extending a sealed interface generated by Jisel.

+ * All sealed interfaces generated by Jisel follow the naming convention: Sealed<ProfileName><LargeInterfaceSimpleName>
+ * See @{@link AddToProfiles} attributes documentation.

+ */ @Retention(RetentionPolicy.SOURCE) @Target({ElementType.TYPE}) +@Repeatable(AddToProfiles.AddToProfilezz.class) public @interface AddToProfiles { - String[] value(); + + /** + * Not Required - specifies an array containing the names of any of the profiles used with the @{@link SealForProfile} annotations in the large interface definition. + * Each one of the profiles names corresponds to the <ProfileName> from the sealed interfaces naming convention.
+ * If not provided or empty, the annotated class, interface or record will be added to the permits list of the generated parent sealed interface.
+ * Also, each one of the provided profiles names MUST be one of the profiles defined in the large interface definition using @SealForProfile. If not, + * the specified profile will be ignored and an informational message regarding provided incorrect profiles will be printed during the compilation.
+ * + * @return an array of the profiles names + */ + String[] profiles() default EMPTY_STRING; + + /** + * Required - MUST be the Fully Qualified Name of the large interface. That would be the <LargeInterfaceSimpleName> as seen + * in the sealed interface name convention, preceded by the package name.
+ * + * @return the large interface fully qualified name + */ + String largeInterface(); + + /** + * Internal annotation allowing @{@link AddToProfiles} to be repeatable + */ + @Retention(RetentionPolicy.SOURCE) + @Target(ElementType.TYPE) + @interface AddToProfilezz { + /** + * array attribute allowing @{@link AddToProfiles} to be repeatable + * + * @return array of @AddToProfiles instances + */ + AddToProfiles[] value(); + } } \ No newline at end of file diff --git a/src/main/java/org/jisel/JiselAnnotationProcessor.java b/src/main/java/org/jisel/JiselAnnotationProcessor.java index 89890cf..f54981b 100644 --- a/src/main/java/org/jisel/JiselAnnotationProcessor.java +++ b/src/main/java/org/jisel/JiselAnnotationProcessor.java @@ -1,6 +1,32 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package org.jisel; import com.google.auto.service.AutoService; +import org.jisel.generator.SealedInterfaceSourceFileGenerator; +import org.jisel.generator.StringGenerator; +import org.jisel.handlers.AddToProfileHandler; +import org.jisel.handlers.JiselAnnotationHandler; +import org.jisel.handlers.SealForProfileHandler; import javax.annotation.processing.AbstractProcessor; import javax.annotation.processing.Processor; @@ -9,95 +35,122 @@ import javax.annotation.processing.SupportedSourceVersion; import javax.lang.model.SourceVersion; import javax.lang.model.element.Element; -import javax.lang.model.element.ElementKind; import javax.lang.model.element.TypeElement; -import javax.tools.JavaFileObject; import java.io.IOException; -import java.io.PrintWriter; +import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; - -// @Slf4j -@SupportedAnnotationTypes("com.bayor.jisel.annotation.SealFor") +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.lang.String.format; +import static java.util.Collections.unmodifiableMap; +import static java.util.Collections.unmodifiableSet; +import static java.util.stream.Collectors.joining; +import static org.jisel.generator.StringGenerator.ORG_JISEL_ADD_TO_PROFILE; +import static org.jisel.generator.StringGenerator.ORG_JISEL_ADD_TO_PROFILES; +import static org.jisel.generator.StringGenerator.ORG_JISEL_ADD_TO_PROFILEZ; +import static org.jisel.generator.StringGenerator.ORG_JISEL_ADD_TO_PROFILEZZ; +import static org.jisel.generator.StringGenerator.ORG_JISEL_SEAL_FOR_PROFILE; +import static org.jisel.generator.StringGenerator.ORG_JISEL_SEAL_FOR_PROFILES; +import static org.jisel.generator.StringGenerator.ORG_JISEL_SEAL_FOR_PROFILEZ; +import static org.jisel.generator.StringGenerator.ORG_JISEL_SEAL_FOR_PROFILEZZ; + +/** + * Jisel annotation processor class. Picks up and processes all elements annotated with @{@link SealForProfile}, @{@link SealForProfiles}, @{@link AddToProfile} and @{@link AddToProfiles}
+ */ +@SupportedAnnotationTypes({ORG_JISEL_SEAL_FOR_PROFILE, ORG_JISEL_SEAL_FOR_PROFILES, ORG_JISEL_SEAL_FOR_PROFILEZ, ORG_JISEL_SEAL_FOR_PROFILEZZ, + ORG_JISEL_ADD_TO_PROFILE, ORG_JISEL_ADD_TO_PROFILES, ORG_JISEL_ADD_TO_PROFILEZ, ORG_JISEL_ADD_TO_PROFILEZZ}) @SupportedSourceVersion(SourceVersion.RELEASE_17) @AutoService(Processor.class) -public class JiselAnnotationProcessor extends AbstractProcessor { - - @Override - public boolean process(Set annotations, RoundEnvironment roundEnvironment) { - - for (TypeElement annotation : annotations) { - - if (!annotation.getSimpleName().toString().contains("SealFor")) - continue; +public class JiselAnnotationProcessor extends AbstractProcessor implements StringGenerator { - Set annotatedElements = roundEnvironment.getElementsAnnotatedWith(annotation); + private final Logger log = Logger.getLogger(JiselAnnotationProcessor.class.getName()); - System.out.println(">>>>>>>> annotatedElements >>>>>>>>>>>" + annotatedElements); + private final JiselAnnotationHandler sealForProfileHandler; - List annotatedClasses = annotatedElements.stream() - //.filter(element -> element.getClass().getClassLoader().isRecord()) - .filter(element -> ElementKind.INTERFACE.equals(element.getKind())) - .toList(); + private final JiselAnnotationHandler addToProfileHandler; - System.out.println(">>>>>>>> annotatedClasses >>>>>>>>>>>" + annotatedClasses); + private final SealedInterfaceSourceFileGenerator sealedInterfaceSourceFileGenerator; - List annotatedMethods = annotatedElements.stream() - .filter(element -> !element.getClass().isRecord()) - .filter(element -> ElementKind.METHOD.equals(element.getKind())) - .toList(); + /** + * JiselAnnotationProcessor constructor. Initializes needed instances of {@link SealForProfileHandler}, {@link AddToProfileHandler} and {@link SealedInterfaceSourceFileGenerator} + */ + public JiselAnnotationProcessor() { + this.sealForProfileHandler = new SealForProfileHandler(); + this.addToProfileHandler = new AddToProfileHandler(); + this.sealedInterfaceSourceFileGenerator = new SealedInterfaceSourceFileGenerator(); + } - System.out.println(">>>>>>>> annotateMethods >>>>>>>>>>>" + annotatedMethods); + @Override + public boolean process(final Set annotations, final RoundEnvironment roundEnv) { + + var allAnnotatedSealForProfileElements = new HashSet(); + var allAnnotatedAddToProfileElements = new HashSet(); + var sealedInterfacesToGenerateByLargeInterface = new HashMap>>(); + var sealedInterfacesPermitsByLargeInterface = new HashMap>>(); + + populateAllAnnotatedElementsSets(annotations, roundEnv, allAnnotatedSealForProfileElements, allAnnotatedAddToProfileElements); + + // process all interface methods annotated with @SealForProfile + var statusReport = sealForProfileHandler.handleAnnotatedElements( + processingEnv, + unmodifiableSet(allAnnotatedSealForProfileElements), + sealedInterfacesToGenerateByLargeInterface, + sealedInterfacesPermitsByLargeInterface + ); + displayStatusReport(statusReport, SEAL_FOR_PROFILE); + + // process all child classes or interfaces annotated with @AddToProfile + statusReport = addToProfileHandler.handleAnnotatedElements( + processingEnv, + unmodifiableSet(allAnnotatedAddToProfileElements), + unmodifiableMap(sealedInterfacesToGenerateByLargeInterface), + sealedInterfacesPermitsByLargeInterface + ); + displayStatusReport(statusReport, ADD_TO_PROFILE); + + try { + var generatedFiles = sealedInterfaceSourceFileGenerator.createSourceFiles( + processingEnv, + sealedInterfacesToGenerateByLargeInterface, + sealedInterfacesPermitsByLargeInterface + ); + if (!generatedFiles.isEmpty()) { + log.info(() -> format("%s:%n%s", FILE_GENERATION_SUCCESS, generatedFiles.stream().collect(joining(format("%n"))))); + } + } catch (IOException e) { + log.log(Level.SEVERE, FILE_GENERATION_ERROR, e); } return true; } - private void writeSealedInterfaceFile(String className, List gettersList, Map getterMap, Set allAnnotatedElements) throws IOException { - String recordClassString = buildSealedInterfaceContent(className, gettersList, getterMap, allAnnotatedElements); - JavaFileObject recordClassFile = processingEnv.getFiler().createSourceFile(className + "Record"); - try (PrintWriter out = new PrintWriter(recordClassFile.openWriter())) { - out.println(recordClassString); + private void populateAllAnnotatedElementsSets(final Set annotations, + final RoundEnvironment roundEnv, + final Set allAnnotatedSealForProfileElements, + final Set allAnnotatedAddToProfileElements) { + for (var annotation : annotations) { + if (annotation.getSimpleName().toString().contains(SEAL_FOR_PROFILE)) { + allAnnotatedSealForProfileElements.addAll(roundEnv.getElementsAnnotatedWith(annotation)); + } + if (annotation.getSimpleName().toString().contains(ADD_TO_PROFILE)) { + allAnnotatedAddToProfileElements.addAll(roundEnv.getElementsAnnotatedWith(annotation)); + } } } - private String buildSealedInterfaceContent(String className, List gettersList, Map getterMap, Set allAnnotatedElements) { - - StringBuilder recordClassContent = new StringBuilder(); - - String packageName = null; - - int lastDot = className.lastIndexOf('.'); - - if (lastDot > 0) { - packageName = className.substring(0, lastDot); + private void displayStatusReport(final Map statusReport, final String annotationName) { + if (!statusReport.values().stream().collect(joining()).isBlank()) { + var output = new StringBuilder(format("%n%s - @%s%n", STATUS_REPORT_TITLE, annotationName)); + statusReport.entrySet().forEach(mapEntry -> { + if (!mapEntry.getValue().isBlank()) { + output.append(format("\t> %s: %s%n", mapEntry.getKey().toString(), mapEntry.getValue())); + } + }); + log.warning(output::toString); } - - String simpleClassName = className.substring(lastDot + 1); - String recordClassName = className + "Record"; - String recordSimpleClassName = recordClassName.substring(lastDot + 1); - - if (packageName != null) { - recordClassContent.append("package "); - recordClassContent.append(packageName); - recordClassContent.append(";"); - recordClassContent.append("\n\n"); - } - - recordClassContent.append("public record "); - recordClassContent.append(recordSimpleClassName); - recordClassContent.append("("); - -// // System.out.println("################################" + recordSimpleClassName); -// buildRecordAttributesFromGettersList(recordClassContent, getterMap, gettersList, allAnnotatedElements); - - recordClassContent.append(") {\n\n"); - - // buildRecordCustom1ArgConstructor(recordClassContent, simpleClassName, gettersList, allAnnotatedElements); - - recordClassContent.append("}"); - - return recordClassContent.toString(); } } \ No newline at end of file diff --git a/src/main/java/org/jisel/SealForProfile.java b/src/main/java/org/jisel/SealForProfile.java index 9a9a628..ed0c289 100644 --- a/src/main/java/org/jisel/SealForProfile.java +++ b/src/main/java/org/jisel/SealForProfile.java @@ -1,3 +1,24 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package org.jisel; import java.lang.annotation.ElementType; @@ -6,16 +27,37 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Annotation to be applied only on top of abstract methods of an interface you intend to segregate.

+ * For the specified profile name, a sealed interface will be generated following the naming convention: + * Sealed<ProfileName><LargeInterfaceSimpleName>

+ * <LargeInterfaceSimpleName> corresponds to the simplename of the interface being segregated.

+ * See @{@link SealForProfile} attributes documentation.

+ */ @Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD}) -@Repeatable(SealForProfile.SealForProfiless.class) +@Repeatable(SealForProfile.SealForProfilez.class) public @interface SealForProfile { + /** + * Profile name to use while segregation the large interface. A sealed interface file will be generated following the naming convention: + * Sealed<ProfileName><LargeInterfaceSimpleName> + * + * @return a string containing the profile name + */ String value(); + /** + * Internal annotation allowing @{@link SealForProfile} to be repeatable + */ @Retention(RetentionPolicy.SOURCE) @Target(ElementType.METHOD) - @interface SealForProfiless { + @interface SealForProfilez { + /** + * array attribute allowing @{@link SealForProfile} to be repeatable + * + * @return array of @SealForProfile instances + */ SealForProfile[] value(); } } \ No newline at end of file diff --git a/src/main/java/org/jisel/SealForProfiles.java b/src/main/java/org/jisel/SealForProfiles.java index 93294c9..7160cd0 100644 --- a/src/main/java/org/jisel/SealForProfiles.java +++ b/src/main/java/org/jisel/SealForProfiles.java @@ -1,12 +1,62 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package org.jisel; import java.lang.annotation.ElementType; +import java.lang.annotation.Repeatable; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +/** + * Annotation to be applied only on top of abstract methods of an interface you intend to segregate.

+ * For each one of the specified profiles names, a sealed interface will be generated following the naming convention: Sealed<ProfileName><LargeInterfaceSimpleName>

+ * <LargeInterfaceSimpleName> corresponds to the simplename of the interface being segregated.

+ * See @{@link SealForProfiles} attributes documentation.

+ */ @Retention(RetentionPolicy.SOURCE) @Target({ElementType.METHOD}) +@Repeatable(SealForProfiles.SealForProfilezz.class) public @interface SealForProfiles { + + /** + * Profile name to use while segregation the large interface. A sealed interface file will be generated following the naming convention: + * Sealed<ProfileName><LargeInterfaceSimpleName> + * + * @return an array of the profiles names + */ String[] value(); + + /** + * Internal annotation allowing @{@link SealForProfiles} to be repeatable + */ + @Retention(RetentionPolicy.SOURCE) + @Target(ElementType.METHOD) + @interface SealForProfilezz { + /** + * array attribute allowing @{@link SealForProfiles} to be repeatable + * + * @return array of @SealForProfiles instances + */ + SealForProfiles[] value(); + } } \ No newline at end of file diff --git a/src/main/java/org/jisel/generator/CodeGenerator.java b/src/main/java/org/jisel/generator/CodeGenerator.java new file mode 100644 index 0000000..dee78ce --- /dev/null +++ b/src/main/java/org/jisel/generator/CodeGenerator.java @@ -0,0 +1,239 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jisel.generator; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.type.ExecutableType; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import java.util.Set; + +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.joining; + +/** + * Exposes contract to be fulfilled by a class generating the code of a sealed interface + */ +public sealed interface CodeGenerator permits JavaxGeneratedGenerator, ExtendsGenerator, PermitsGenerator, MethodsGenerator { + + /** + * Generates piece of code requested, based on the parameters provided in the params object and appends it to the provided classOrInterfaceContent param + * + * @param classOrInterfaceContent Stringbuilder object containing the sealed interface code being generated + * @param params expected parameters + */ + void generateCode(StringBuilder classOrInterfaceContent, List params); +} + +/** + * Exposes contract to be fulfilled by a class generating the "extends" clause of a sealed interface definition, along with + * the list of the parent classes or interfaces + */ +sealed interface ExtendsGenerator extends CodeGenerator, StringGenerator permits SealedInterfaceExtendsGenerator { + + /** + * Generates the "extends" clause of a sealed interface being generated, along with the list of parent interfaces, based on + * a provided {@link Map} containing parents/subtypes information (the permits Map) and the name of the profile for which the + * sealed interface will be generated + * + * @param processingEnvironment {@link ProcessingEnvironment} object, needed to access low-level information regarding the used annotations + * @param sealedInterfaceContent {@link StringBuilder} object containing the sealed interface code being generated + * @param permitsMap {@link Map} containing parents/subtypes information. The Map key is the profile name whose generated + * sealed interface will be a parent interface, while the value is the list of profiles names whose + * sealed interfaces will be generated as subtypes + * @param processedProfile name of the profile whose sealed interface is being generated + * @param largeInterfaceElement {@link Element} instance of the large interface being segregated + */ + void generateExtendsClauseFromPermitsMapAndProcessedProfile( + ProcessingEnvironment processingEnvironment, + StringBuilder sealedInterfaceContent, + Map> permitsMap, + String processedProfile, + Element largeInterfaceElement + ); + + @Override + default void generateCode(final StringBuilder classOrInterfaceContent, final List params) { + classOrInterfaceContent.append(format( + " %s %s ", + isInterface(classOrInterfaceContent.toString()) ? EXTENDS : IMPLEMENTS, + params.stream().map(StringGenerator::removeCommaSeparator).collect(joining(COMMA_SEPARATOR + WHITESPACE)) + )); + } + + private boolean isInterface(final String classOrInterfaceContent) { + return classOrInterfaceContent.contains(INTERFACE + WHITESPACE) && !classOrInterfaceContent.contains(CLASS + WHITESPACE); + } +} + +/** + * Exposes contract to be fulfilled by a class generating the "permits" clause of a sealed interface definition, along with + * the list of the subtypes classes or interfaces permitted by the sealed interface being generated + */ +sealed interface PermitsGenerator extends CodeGenerator, StringGenerator permits SealedInterfacePermitsGenerator { + + /** + * Generates the "permits" clause of a sealed interface being generated, along with the list of parent interfaces, based on + * a provided {@link Map} containing parents/subtypes information (the permits Map) and the name of the profile for which the + * sealed interface will be generated + * + * @param sealedInterfaceContent {@link StringBuilder} object containing the sealed interface code being generated + * @param permitsMap {@link Map} containing parents/subtypes information. The Map key is the profile name whose generated + * sealed interface will be a parent interface, while the value is the list of profiles names whose + * sealed interfaces will be generated as subtypes + * @param processedProfile name of the profile whose sealed interface is being generated + * @param largeInterfaceElement {@link Element} instance of the large interface being segregated + */ + void generatePermitsClauseFromPermitsMapAndProcessedProfile( + StringBuilder sealedInterfaceContent, + Map> permitsMap, + String processedProfile, + Element largeInterfaceElement + ); + + @Override + default void generateCode(final StringBuilder classOrInterfaceContent, final List params) { + classOrInterfaceContent.append(format( + " %s %s ", + PERMITS, + params.stream().map(StringGenerator::removeCommaSeparator).collect(joining(COMMA_SEPARATOR + WHITESPACE)) + )); + } + + /** + * Adds a generated final class to the {@link Map} containing parents/subtypes information, only for the sealed interfaces at the + * lowest-level of the generated hierarchy (also known as childless interfaces).
+ * Practice proper to Jisel only to avoid compilation errors for sealed interfaces not having any existing subtypes + * + * @param permitsMap {@link Map} containing parents/subtypes information. The Map key is the profile name whose generated + * sealed interface will be a parent interface, while the value is the list of profiles names whose + * sealed interfaces will be generated as subtypes + * @param largeInterfaceElement {@link Element} instance of the large interface being segregated + */ + default void addFinalClassToPermitsMap(final Map> permitsMap, final Element largeInterfaceElement) { + var finalClassName = UNDERSCORE + largeInterfaceElement.getSimpleName().toString() + FINAL_CLASS_SUFFIX; + var childlessProfiles = permitsMap.values().stream() + .flatMap(Collection::stream) + .distinct() + .filter(childProfileName -> permitsMap.keySet().stream().noneMatch(parentProfile -> parentProfile.equals(childProfileName))) + .filter(childProfileName -> !finalClassName.equals(childProfileName)) // if finalClassName found remove it from the new list + .filter(childProfileName -> !childProfileName.contains(DOT)) // also skip all qualifiedname classes added by @addToProfile + .toList(); + childlessProfiles.forEach(childlessProfile -> permitsMap.put(childlessProfile, asList(finalClassName))); + } +} + +/** + * Exposes contract to be fulfilled by a class generating the list of abstracts methods of a sealed interface being generated.
+ * Same is also used to generate the list of concrete methods of the convenience final class generated for the bottom-level generated sealed interfaces + */ +sealed interface MethodsGenerator extends CodeGenerator, StringGenerator permits SealedInterfaceMethodsGenerator { + + /** + * Generates a list of abstracts methods definitions and appends it to the sealed interface code being generated + * + * @param sealedInterfaceContent {@link StringBuilder} object containing the sealed interface code being generated + * @param methodsSet {@link Set} of {@link Element} instances representing each one of the abstract methods to generate + */ + void generateAbstractMethodsFromElementsSet(StringBuilder sealedInterfaceContent, Set methodsSet); + + /** + * Mainly used for a final class generation.
+ * Generates a list of concrete methods definitions (signature and body), and appends it to the final class being generated + * + * @param sealedInterfaceContent {@link StringBuilder} object containing the sealed interface code being generated + * @param methodsSet {@link Set} of {@link Element} instances representing each one of the abstract methods to generate + */ + void generateEmptyConcreteMethodsFromElementsSet(StringBuilder sealedInterfaceContent, Set methodsSet); + + @Override + default void generateCode(final StringBuilder classOrInterfaceContent, final List params) { + params.forEach(methodDefinition -> classOrInterfaceContent.append(format("\t%s%n", methodDefinition))); + } + + /** + * Checks whether the provided method {@link Element} instance has any arguments + * + * @param methodElement method {@link Element} instance + * @return true if the provided method has any arguments, false otherwise + */ + default boolean methodHasArguments(final Element methodElement) { + return methodElement.toString().indexOf(CLOSING_PARENTHESIS) - methodElement.toString().indexOf(OPENING_PARENTHESIS) > 1; + } + + /** + * Returns a string representing the fully qualified name of a method return type + * + * @param methodElement method {@link Element} instance + * @return a string representing the fully qualified name of the provided method's return type + */ + default String generateReturnType(final Element methodElement) { + return ((ExecutableType) methodElement.asType()).getReturnType().toString(); + } + + /** + * Constructs a string containing a method name and parameters formatted as in the method signature + * + * @param methodElement method {@link Element} instance + * @return a string containing a method name and parameters formatted as in the method signature + */ + default String generateMethodNameAndParameters(final Element methodElement) { + var output = methodElement.toString(); + if (methodHasArguments(methodElement)) { + int paramIdx = 0; + while (output.contains(COMMA_SEPARATOR)) { + output = output.replace(COMMA_SEPARATOR, WHITESPACE + PARAMETER_PREFIX + paramIdx + TEMP_PLACEHOLDER + WHITESPACE); + paramIdx++; + } + output = output.replace(CLOSING_PARENTHESIS, WHITESPACE + PARAMETER_PREFIX + paramIdx + CLOSING_PARENTHESIS).replaceAll(TEMP_PLACEHOLDER, COMMA_SEPARATOR); + } + return output; + } + + /** + * Constructs a string containing a comma-separated list of exceptions classes thrown by the provided method {@link Element} instance + * + * @param methodElement method {@link Element} instance + * @return a comma-separated list of exceptions classes thrown by the provided method {@link Element} instance + */ + default String generateThrownExceptions(final Element methodElement) { + return ((ExecutableType) methodElement.asType()).getThrownTypes().stream().map(Object::toString).collect(joining(COMMA_SEPARATOR + WHITESPACE)); + } + + /** + * Constructs a string containing the default value for the provided method's return type + * + * @param methodElement method {@link Element} instance + * @return a string containing the default value for the provided method's return type + */ + default String generateDefaultReturnValueForMethod(final Element methodElement) { + return switch (((ExecutableType) methodElement.asType()).getReturnType().getKind()) { + case BOOLEAN -> RETURN + WHITESPACE + DEFAULT_BOOLEAN_VALUE; + case VOID -> RETURN; + case BYTE, SHORT, INT, LONG, FLOAT, DOUBLE, CHAR -> RETURN + WHITESPACE + DEFAULT_NUMBER_VALUE; + default -> RETURN + WHITESPACE + DEFAULT_NULL_VALUE; + }; + } +} \ No newline at end of file diff --git a/src/main/java/org/jisel/generator/FinalClassContentGenerator.java b/src/main/java/org/jisel/generator/FinalClassContentGenerator.java new file mode 100644 index 0000000..1dfb540 --- /dev/null +++ b/src/main/java/org/jisel/generator/FinalClassContentGenerator.java @@ -0,0 +1,103 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jisel.generator; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.TypeElement; +import java.util.List; +import java.util.Map; + +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toSet; + +/** + * Generates content for a final class.
+ * A final class is always generated by Jisel for the bottom-level generated sealed interfaces.
+ * The generated final class implements the provided large interface and implements all its methods by providing default return values for each. + */ +public class FinalClassContentGenerator implements StringGenerator { + + private final CodeGenerator javaxGeneratedGenerator; + private final ExtendsGenerator extendsGenerator; + private final MethodsGenerator methodsGenerator; + + /** + * FinalClassContentGenerator constructor. Instantiates needed instances of {@link JavaxGeneratedGenerator}, {@link SealedInterfaceExtendsGenerator} + * and {@link SealedInterfaceMethodsGenerator} + */ + public FinalClassContentGenerator() { + this.javaxGeneratedGenerator = new JavaxGeneratedGenerator(); + this.extendsGenerator = new SealedInterfaceExtendsGenerator(); + this.methodsGenerator = new SealedInterfaceMethodsGenerator(); + } + + /** + * Generates content of the final class generated for the provided large interface + * + * @param processingEnvironment {@link ProcessingEnvironment} object, needed to access low-level information regarding the used annotations + * @param largeInterfaceElement {@link Element} instance of the large interface being segregated + * @param sealedInterfacesPermitsMap {@link Map} containing information about the subtypes permitted by each one of the sealed interfaces to be generated + * @return the final class string content + */ + public String generateFinalClassContent( + final ProcessingEnvironment processingEnvironment, + final Element largeInterfaceElement, + final Map> sealedInterfacesPermitsMap) { + var finalClassName = UNDERSCORE + largeInterfaceElement.getSimpleName().toString() + FINAL_CLASS_SUFFIX; + var finalClassContent = new StringBuilder(); + // package name + generatePackageName(largeInterfaceElement).ifPresent(name -> finalClassContent.append(format("%s %s;%n%n", PACKAGE, name))); + // javaxgenerated + javaxGeneratedGenerator.generateCode(finalClassContent, null); + // public final class + finalClassContent.append(format( + "%s %s ", + PUBLIC_FINAL_CLASS, + finalClassName + )); + // list of implements + extendsGenerator.generateExtendsClauseFromPermitsMapAndProcessedProfile( + processingEnvironment, + finalClassContent, + sealedInterfacesPermitsMap, + finalClassName, + largeInterfaceElement + ); + // opening bracket after permits list + finalClassContent.append(format(" %s%n ", OPENING_BRACKET)); + // list of methods + methodsGenerator.generateEmptyConcreteMethodsFromElementsSet( + finalClassContent, + processingEnvironment.getElementUtils().getAllMembers((TypeElement) largeInterfaceElement).stream() + .filter(element -> ElementKind.METHOD.equals(element.getKind())) + .filter(element -> asList(METHODS_TO_EXCLUDE).stream().noneMatch(excludedMeth -> element.toString().contains(excludedMeth + OPENING_PARENTHESIS))) + .collect(toSet()) + ); + // closing bracket + finalClassContent.append(CLOSING_BRACKET); + // + return removeDoubleSpaceOccurrences(finalClassContent.toString()); + } +} \ No newline at end of file diff --git a/src/main/java/org/jisel/generator/helpers/JavaxGeneratedGenerator.java b/src/main/java/org/jisel/generator/JavaxGeneratedGenerator.java similarity index 76% rename from src/main/java/org/jisel/generator/helpers/JavaxGeneratedGenerator.java rename to src/main/java/org/jisel/generator/JavaxGeneratedGenerator.java index 1d5eedf..0f153d5 100644 --- a/src/main/java/org/jisel/generator/helpers/JavaxGeneratedGenerator.java +++ b/src/main/java/org/jisel/generator/JavaxGeneratedGenerator.java @@ -1,5 +1,5 @@ /** - * Copyright (c) 2021 Mohamed Ashraf Bayor + * Copyright (c) 2022 Mohamed Ashraf Bayor *

* Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal @@ -19,21 +19,20 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package org.jisel.generator.helpers; +package org.jisel.generator; import org.jisel.JiselAnnotationProcessor; import java.io.IOException; import java.time.ZonedDateTime; import java.time.format.DateTimeFormatter; -import java.util.Map; +import java.util.List; import java.util.Properties; import static java.lang.String.format; /** - * Generates the @javax.annotation.processing.Generated annotation section at the top of the generated record class with the attributes: value, date and comments
- * The generateRecord() method params map is not required + * Generates the {@link javax.annotation.processing.Generated} annotation section at the top of the generated interfaces or classes with the attributes: value, date and comments
*/ public final class JavaxGeneratedGenerator implements CodeGenerator { @@ -46,10 +45,10 @@ private void buildGeneratedAnnotationSection(final StringBuilder recordClassCont date = "%s", comments = "version: %s" ) - """ - , JiselAnnotationProcessor.class.getName() - , ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME) - , getAppVersion() + """, + JiselAnnotationProcessor.class.getName(), + ZonedDateTime.now().format(DateTimeFormatter.ISO_OFFSET_DATE_TIME), + getAppVersion() )); } @@ -65,7 +64,7 @@ private String getAppVersion() { } @Override - public void generateCode(final StringBuilder recordClassContent, final Map params) { - buildGeneratedAnnotationSection(recordClassContent); + public void generateCode(final StringBuilder classOrInterfaceContent, final List params) { + buildGeneratedAnnotationSection(classOrInterfaceContent); } } \ No newline at end of file diff --git a/src/main/java/org/jisel/generator/ReportGenerator.java b/src/main/java/org/jisel/generator/ReportGenerator.java new file mode 100644 index 0000000..924f863 --- /dev/null +++ b/src/main/java/org/jisel/generator/ReportGenerator.java @@ -0,0 +1,91 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jisel.generator; + +import javax.lang.model.element.Element; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static java.lang.String.format; +import static java.util.stream.Collectors.joining; + +/** + * Generates a Report file listing all generated sealed interfaces for the provided large interfaces.
+ * Sample report:
+ * com.bayor.jisel.annotation.client.hierarchicalinheritance.Sociable
+ * Created sealed interfaces:
+ * SealedActiveWorkerSociable
+ * - Children:
+ * _SociableFinalCass
+ * SealedWorkerSociable
+ * - Children:
+ * SealedActiveWorkerSociable
+ * com.bayor.jisel.annotation.client.hierarchicalinheritance.subclasses.StudentWorkerHybrid
+ * SealedStudentSociable
+ * - Children:
+ * com.bayor.jisel.annotation.client.hierarchicalinheritance.subclasses.StudentWorkerHybrid
+ * SealedSociable
+ * - Children:
+ * SealedWorkerSociable
+ * SealedStudentSociable
+ */ +public class ReportGenerator implements StringGenerator { + + /** + * Generates a Report file listing all generated sealed interfaces for the provided large interfaces. + * + * @param largeInterfaceElement {@link Element} instance of the large interface being segregated + * @param sealedInterfacesToGenerateMap {@link Map} containing information about the sealed interfaces to be generated + * @param sealedInterfacesPermitsMap {@link Map} containing information about the subtypes permitted by each one of the sealed interfaces to be generated + * @return a string containing the text report + */ + public String generateReportForBloatedInterface(final Element largeInterfaceElement, + final Map> sealedInterfacesToGenerateMap, + final Map> sealedInterfacesPermitsMap) { + var reportContent = new StringBuilder(); + var packageNameOpt = generatePackageName(largeInterfaceElement); + var qualifiedName = packageNameOpt.isPresent() + ? packageNameOpt.get() + DOT + largeInterfaceElement.getSimpleName().toString() + : largeInterfaceElement.getSimpleName().toString(); + reportContent.append(format("%s%n", qualifiedName)); + reportContent.append(format("%s%n", JISEL_REPORT_CREATED_SEALED_INTERFACES_HEADER)); + sealedInterfacesToGenerateMap.entrySet().forEach(entrySet -> { + var sealedInterfaceName = sealedInterfaceNameConvention(entrySet.getKey(), largeInterfaceElement); + reportContent.append(format("\t%s%n", sealedInterfaceName)); + var sealedInterfaceChildrenOpt = Optional.ofNullable(sealedInterfacesPermitsMap.get(entrySet.getKey())); + if (sealedInterfaceChildrenOpt.isPresent() && !sealedInterfaceChildrenOpt.get().isEmpty()) { + reportContent.append(format("\t - %s%n", JISEL_REPORT_CHILDREN_HEADER)); + if (!sealedInterfaceChildrenOpt.get().isEmpty()) { + reportContent.append(format( + "\t\t%s%n", + sealedInterfaceChildrenOpt.get().stream() + .map(childName -> sealedInterfaceNameConvention(childName, largeInterfaceElement)) + .collect(joining(format("%n\t\t"))) + )); + } + } + }); + return reportContent.toString(); + } +} \ No newline at end of file diff --git a/src/main/java/org/jisel/generator/SealedInterfaceContentGenerator.java b/src/main/java/org/jisel/generator/SealedInterfaceContentGenerator.java new file mode 100644 index 0000000..68503d6 --- /dev/null +++ b/src/main/java/org/jisel/generator/SealedInterfaceContentGenerator.java @@ -0,0 +1,186 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jisel.generator; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static java.lang.String.format; + +/** + * Generates the content of a sealed interface + */ +public final class SealedInterfaceContentGenerator implements StringGenerator { + + private final CodeGenerator javaxGeneratedGenerator; + private final ExtendsGenerator extendsGenerator; + private final PermitsGenerator permitsGenerator; + private final MethodsGenerator methodsGenerator; + + /** + * SealedInterfaceContentGenerator constructor. Instantiates needed instances of {@link JavaxGeneratedGenerator}, {@link SealedInterfaceExtendsGenerator}, + * {@link SealedInterfacePermitsGenerator} and {@link SealedInterfaceMethodsGenerator} + */ + public SealedInterfaceContentGenerator() { + this.javaxGeneratedGenerator = new JavaxGeneratedGenerator(); + this.extendsGenerator = new SealedInterfaceExtendsGenerator(); + this.permitsGenerator = new SealedInterfacePermitsGenerator(); + this.methodsGenerator = new SealedInterfaceMethodsGenerator(); + } + + /** + * Generates the content of a sealed interface + * + * @param processingEnvironment {@link ProcessingEnvironment} object, needed to access low-level information regarding the used annotations + * @param sealedInterfacesToGenerateMapEntrySet {@link java.util.Map.Entry} instance containing information about the sealed interfaces to be generated + * @param largeInterfaceElement {@link Element} instance of the large interface being segregated + * @param sealedInterfacesPermitsMap Map containing information about the subtypes permitted by each one of the sealed interfaces to be generated + * @return the string content of the sealed interface to generate + */ + public String generateSealedInterfaceContent(final ProcessingEnvironment processingEnvironment, + final Map.Entry> sealedInterfacesToGenerateMapEntrySet, + final Element largeInterfaceElement, + final Map> sealedInterfacesPermitsMap) { + var sealedInterfaceContent = new StringBuilder(); + // package name + generatePackageName(largeInterfaceElement).ifPresent(name -> sealedInterfaceContent.append(format("%s %s;%n%n", PACKAGE, name))); + // javaxgenerated + javaxGeneratedGenerator.generateCode(sealedInterfaceContent, null); + // public sealed interface + var profile = sealedInterfacesToGenerateMapEntrySet.getKey(); + sealedInterfaceContent.append(format( + "%s %s ", + PUBLIC_SEALED_INTERFACE, + sealedInterfaceNameConvention(profile, largeInterfaceElement) + )); + // list of extends + extendsGenerator.generateExtendsClauseFromPermitsMapAndProcessedProfile(processingEnvironment, sealedInterfaceContent, sealedInterfacesPermitsMap, profile, largeInterfaceElement); + // list of permits + permitsGenerator.generatePermitsClauseFromPermitsMapAndProcessedProfile(sealedInterfaceContent, sealedInterfacesPermitsMap, profile, largeInterfaceElement); + // opening bracket after permits list + sealedInterfaceContent.append(format(" %s%n ", OPENING_BRACKET)); + // list of methods + methodsGenerator.generateAbstractMethodsFromElementsSet(sealedInterfaceContent, sealedInterfacesToGenerateMapEntrySet.getValue()); + // closing bracket + sealedInterfaceContent.append(CLOSING_BRACKET); + // + return removeDoubleSpaceOccurrences(sealedInterfaceContent.toString()); + } +} + +/** + * Generates the "extends" clause of a sealed interface definition, along with the list of the parent interfaces + */ +final class SealedInterfaceExtendsGenerator implements ExtendsGenerator { + @Override + public void generateExtendsClauseFromPermitsMapAndProcessedProfile(final ProcessingEnvironment processingEnvironment, + final StringBuilder sealedInterfaceContent, + final Map> permitsMap, + final String processedProfile, + final Element largeInterfaceElement) { + Optional.ofNullable(permitsMap).ifPresent(nonNullPermitsMap -> { + var parentList = nonNullPermitsMap.entrySet().stream() + .filter(permitsMapEntry -> permitsMapEntry.getValue().contains(processedProfile)) + .map(permitsMapEntry -> sealedInterfaceNameConvention(permitsMapEntry.getKey(), largeInterfaceElement)) + .toList(); + if (!parentList.isEmpty()) { + generateCode(sealedInterfaceContent, parentList); + } else { + // only for largeInterface sealed interface generation, add interfaces it extends if any + var superInterfacesList = processingEnvironment.getTypeUtils().directSupertypes(largeInterfaceElement.asType()).stream() + .map(Object::toString) + .filter(superType -> !superType.contains(JAVA_LANG_OBJECT)) + .toList(); + if (largeInterfaceElement.getSimpleName().toString().equals(processedProfile) && !superInterfacesList.isEmpty()) { + generateCode( + sealedInterfaceContent, + superInterfacesList + ); + } + } + }); + } +} + +/** + * Generates the "permits" clause of a sealed interface definition, along with the list of the subtypes classes or + * interfaces permitted by the sealed interface being generated + */ +final class SealedInterfacePermitsGenerator implements PermitsGenerator { + @Override + public void generatePermitsClauseFromPermitsMapAndProcessedProfile(final StringBuilder sealedInterfaceContent, + final Map> permitsMap, + final String processedProfile, + final Element largeInterfaceElement) { + addFinalClassToPermitsMap(permitsMap, largeInterfaceElement); + var permitsMapOpt = Optional.ofNullable(permitsMap); + if (permitsMapOpt.isPresent() && !permitsMapOpt.get().isEmpty()) { + Optional.ofNullable(permitsMapOpt.get().get(processedProfile)).ifPresent( + childrenList -> generateCode(sealedInterfaceContent, sealedInterfaceNameConventionForList(childrenList, largeInterfaceElement)) + ); + } + } +} + +/** + * Generates the list of abstracts methods of a sealed interface being generated + */ +final class SealedInterfaceMethodsGenerator implements MethodsGenerator { + + @Override + public void generateAbstractMethodsFromElementsSet(final StringBuilder sealedInterfaceContent, final Set methodsSet) { + generateCode( + sealedInterfaceContent, + methodsSet.stream() + .map(element -> format( + "%s %s%s", + generateReturnType(element), + generateMethodNameAndParameters(element), + generateThrownExceptions(element).isEmpty() + ? SEMICOLON + : format(" throws %s", generateThrownExceptions(element) + SEMICOLON) + )) + .toList() + ); + } + + @Override + public void generateEmptyConcreteMethodsFromElementsSet(final StringBuilder sealedInterfaceContent, final Set methodsSet) { + generateCode( + sealedInterfaceContent, + methodsSet.stream() + .map(methodElement -> format( + "public %s %s %s", + generateReturnType(methodElement), + generateMethodNameAndParameters(methodElement), + generateThrownExceptions(methodElement).isEmpty() + ? OPENING_BRACKET + generateDefaultReturnValueForMethod(methodElement) + SEMICOLON + CLOSING_BRACKET + : format("throws %s", generateThrownExceptions(methodElement) + WHITESPACE + OPENING_BRACKET + generateDefaultReturnValueForMethod(methodElement) + SEMICOLON + CLOSING_BRACKET) + )) + .toList() + ); + } +} \ No newline at end of file diff --git a/src/main/java/org/jisel/generator/SealedInterfaceSourceFileGenerator.java b/src/main/java/org/jisel/generator/SealedInterfaceSourceFileGenerator.java index b426f83..7860490 100644 --- a/src/main/java/org/jisel/generator/SealedInterfaceSourceFileGenerator.java +++ b/src/main/java/org/jisel/generator/SealedInterfaceSourceFileGenerator.java @@ -1,4 +1,144 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ package org.jisel.generator; -public class SealedInterfaceSourceFileGenerator { -} +import javax.annotation.processing.FilerException; +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.tools.StandardLocation; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; + +/** + * Creates the content of a sealed interface and writes it to the filesystem.
+ * The sealed interface generation process also includes the final class and report generation + */ +public class SealedInterfaceSourceFileGenerator implements StringGenerator { + + private final SealedInterfaceContentGenerator sealedInterfaceContentGenerator; + private final FinalClassContentGenerator finalClassContentGenerator; + private final ReportGenerator reportGenerator; + + /** + * SealedInterfaceSourceFileGenerator constructor. Creates needed instances of {@link SealedInterfaceContentGenerator}, + * {@link FinalClassContentGenerator} and {@link ReportGenerator} + */ + public SealedInterfaceSourceFileGenerator() { + this.sealedInterfaceContentGenerator = new SealedInterfaceContentGenerator(); + this.finalClassContentGenerator = new FinalClassContentGenerator(); + this.reportGenerator = new ReportGenerator(); + } + + /** + * @param processingEnvironment {@link ProcessingEnvironment} object, needed to access low-level information regarding the used annotations + * @param sealedInterfacesToGenerateByLargeInterface {@link Map} containing information about the sealed interfaces to be generated. + * To be populated and/or modified if needed. The key represents the {@link Element} instance of + * each one of the large interfaces to be segregated, while the associated value is + * a Map of profile name as the key and a Set of Element instances as the value. + * The Element instances represent each one of the abstract methods to be + * added to the generated sealed interface corresponding to a profile. + * @param sealedInterfacesPermitsByLargeInterface Map containing information about the subtypes permitted by each one of the sealed interfaces to be generated. + * To be populated and/or modified if needed. The key represents the Element instance of + * each one of the large interfaces to be segregated, while the associated value is + * a Map of profile name as the key and a List of profiles names as the value. + * @return List of all created source files + * @throws IOException if an I/O error occured + */ + public List createSourceFiles(final ProcessingEnvironment processingEnvironment, + final Map>> sealedInterfacesToGenerateByLargeInterface, + final Map>> sealedInterfacesPermitsByLargeInterface) throws IOException { + var generatedFiles = new ArrayList(); + for (var sealedInterfacesToGenerateMapEntrySet : sealedInterfacesToGenerateByLargeInterface.entrySet()) { + var largeInterfaceElement = sealedInterfacesToGenerateMapEntrySet.getKey(); + for (var sealedInterfacesToGenerate : sealedInterfacesToGenerateMapEntrySet.getValue().entrySet()) { + var profile = sealedInterfacesToGenerate.getKey(); + var generatedSealedInterfaceName = sealedInterfaceNameConvention(profile, largeInterfaceElement); + createSealedInterfaceFile(processingEnvironment, generatedFiles, sealedInterfacesPermitsByLargeInterface.get(largeInterfaceElement), largeInterfaceElement, sealedInterfacesToGenerate, generatedSealedInterfaceName); + } + createFinalClassFile(processingEnvironment, generatedFiles, largeInterfaceElement, sealedInterfacesPermitsByLargeInterface.get(largeInterfaceElement)); + createJiselReportFile(processingEnvironment, generatedFiles, largeInterfaceElement, sealedInterfacesToGenerateByLargeInterface.get(largeInterfaceElement), sealedInterfacesPermitsByLargeInterface.get(largeInterfaceElement)); + } + return generatedFiles; + } + + private void createSealedInterfaceFile(final ProcessingEnvironment processingEnvironment, + final List generatedFiles, + final Map> sealedInterfacesPermitsMap, + final Element largeInterfaceElement, + final Map.Entry> sealedInterfacesToGenerateMapEntrySet, + final String generatedSealedInterfaceName) throws IOException { + var packageNameOpt = generatePackageName(largeInterfaceElement); + try { + var qualifiedName = packageNameOpt.isPresent() ? packageNameOpt.get() + DOT + generatedSealedInterfaceName : generatedSealedInterfaceName; + var fileObject = processingEnvironment.getFiler().createSourceFile(qualifiedName); + try (var out = new PrintWriter(fileObject.openWriter())) { + out.println(sealedInterfaceContentGenerator.generateSealedInterfaceContent(processingEnvironment, sealedInterfacesToGenerateMapEntrySet, largeInterfaceElement, sealedInterfacesPermitsMap)); + } + generatedFiles.add(qualifiedName); + } catch (FilerException e) { + // File was already generated - do nothing + } + } + + private void createFinalClassFile(final ProcessingEnvironment processingEnvironment, + final List generatedFiles, + final Element largeInterfaceElement, + final Map> sealedInterfacesPermitsMap) throws IOException { + var packageNameOpt = generatePackageName(largeInterfaceElement); + try { + var qualifiedName = packageNameOpt.isPresent() + ? packageNameOpt.get() + DOT + UNDERSCORE + largeInterfaceElement.getSimpleName().toString() + FINAL_CLASS_SUFFIX + : UNDERSCORE + largeInterfaceElement.getSimpleName().toString() + FINAL_CLASS_SUFFIX; + var fileObject = processingEnvironment.getFiler().createSourceFile(qualifiedName); + try (var out = new PrintWriter(fileObject.openWriter())) { + out.println(finalClassContentGenerator.generateFinalClassContent(processingEnvironment, largeInterfaceElement, sealedInterfacesPermitsMap)); + } + generatedFiles.add(qualifiedName); + } catch (FilerException e) { + // File was already generated - do nothing + } + } + + private void createJiselReportFile(final ProcessingEnvironment processingEnvironment, + final List generatedFiles, + final Element largeInterfaceElement, + final Map> sealedInterfacesToGenerateMap, + final Map> sealedInterfacesPermitsMap) throws IOException { + var packageNameOpt = generatePackageName(largeInterfaceElement); + try { + var qualifiedName = packageNameOpt.isPresent() + ? packageNameOpt.get() + DOT + UNDERSCORE + largeInterfaceElement.getSimpleName().toString() + JISEL_REPORT_SUFFIX + : UNDERSCORE + largeInterfaceElement.getSimpleName().toString() + JISEL_REPORT_SUFFIX; + var fileObject = processingEnvironment.getFiler().createResource(StandardLocation.SOURCE_OUTPUT, packageNameOpt.isPresent() ? packageNameOpt.get() : EMPTY_STRING, UNDERSCORE + largeInterfaceElement.getSimpleName().toString() + JISEL_REPORT_SUFFIX); + try (var out = new PrintWriter(fileObject.openWriter())) { + out.println(reportGenerator.generateReportForBloatedInterface(largeInterfaceElement, sealedInterfacesToGenerateMap, sealedInterfacesPermitsMap)); + } + generatedFiles.add(qualifiedName); + } catch (FilerException e) { + // File was already generated - do nothing + } + } +} \ No newline at end of file diff --git a/src/main/java/org/jisel/generator/StringGenerator.java b/src/main/java/org/jisel/generator/StringGenerator.java new file mode 100644 index 0000000..5b93c80 --- /dev/null +++ b/src/main/java/org/jisel/generator/StringGenerator.java @@ -0,0 +1,325 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jisel.generator; + +import javax.lang.model.element.Element; +import java.util.List; +import java.util.Optional; +import java.util.function.UnaryOperator; + +import static java.lang.String.format; +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.joining; + +/** + * A bunch of String literals and commonly used string handling functions + */ +public interface StringGenerator { + + /** + * "," comma separator + */ + String COMMA_SEPARATOR = ","; + /** + * ";" semi-colon + */ + String SEMICOLON = ";"; + /** + * " " whitespace + */ + String WHITESPACE = " "; + /** + * "Sealed" + */ + String SEALED_PREFIX = "Sealed"; + /** + * "package" + */ + String PACKAGE = "package"; + /** + * "interface" + */ + String INTERFACE = "interface"; + /** + * "class" + */ + String CLASS = "class"; + /** + * "public sealed interface" + */ + String PUBLIC_SEALED_INTERFACE = "public sealed interface"; + /** + * "public final class" + */ + String PUBLIC_FINAL_CLASS = "public final class"; + /** + * "extends" + */ + String EXTENDS = "extends"; + /** + * "implements" + */ + String IMPLEMENTS = "implements"; + /** + * "permits" + */ + String PERMITS = "permits"; + /** + * "{" + */ + String OPENING_BRACKET = "{"; + /** + * "}" + */ + String CLOSING_BRACKET = "}"; + /** + * "(" + */ + String OPENING_PARENTHESIS = "("; + /** + * ")" + */ + String CLOSING_PARENTHESIS = ")"; + /** + * "" + */ + String EMPTY_STRING = ""; + /** + * "." + */ + String DOT = "."; + /** + * "param" + */ + String PARAMETER_PREFIX = "param"; + /** + * "@" + */ + String TEMP_PLACEHOLDER = "@"; + /** + * "_" + */ + String UNDERSCORE = "_"; + /** + * "FinalClass" + */ + String FINAL_CLASS_SUFFIX = "FinalCass"; + /** + * "return" + */ + String RETURN = "return"; + /** + * "java.lang.Object" + */ + String JAVA_LANG_OBJECT = "java.lang.Object"; + + /** + * "SealForProfile" + */ + String SEAL_FOR_PROFILE = "SealForProfile"; + /** + * "AddToProfile" + */ + String ADD_TO_PROFILE = "AddToProfile"; + /** + * Regex expression to read the string value of the "profile" attribute + */ + String PROFILE_ATTRIBUTE_REGEX = "profile=\"([^\"]*)\""; + /** + * Regex expression to read the string value of the "largeInterface" attribute + */ + String LARGE_INTERFACE_ATTRIBUTE_REGEX = "largeInterface=\"([^\"]*)\""; + /** + * Regex expression to read any attribute value provided within "" + */ + String ANNOTATION_VALUES_REGEX = "\"([^\"]*)\""; + /** + * Regex expression to read attributes information provided using the {@link org.jisel.AddToProfile} annotation.
+ * Sample value to be parsed by the regex: @org.jisel.AddToProfile(profile="ActiveWorker", largeInterface="com.bayor.jisel.annotation.client.data.Sociable") + */ + String ADD_TO_PROFILE_REGEX = "AddToProfile\\((.*?)\\)"; + /** + * Regex expression to read attributes information provided using the {@link org.jisel.AddToProfiles} annotation.
+ * Sample value to be parsed by the regex: @org.jisel.AddToProfiles(profiles={"Student", "Worker"}, largeInterface="com.bayor.jisel.annotation.client.data.Sociable") + */ + String ADD_TO_PROFILES_REGEX = "AddToProfiles\\((.*?)\\)"; + + /** + * Title of the text report displayed in the logs during compilation.
+ * The report is displayed only when an unexpected scenario was encountered (ex: More than 1 top-level parent interfaces found, profile not existing,...) + */ + String STATUS_REPORT_TITLE = "JISEL GENERATION REPORT"; + + /** + * Displayed only when a "severe" error occurred while a sealed interface file was being generated + */ + String FILE_GENERATION_ERROR = "Error generating sealed interfaces"; + /** + * Displayed as a header while listing the successfully generated files + */ + String FILE_GENERATION_SUCCESS = "Successfully generated"; + + /** + * Fully qualified name of the {@link org.jisel.SealForProfile} annotation + */ + String ORG_JISEL_SEAL_FOR_PROFILE = "org.jisel.SealForProfile"; + /** + * Fully qualified name of the {@link org.jisel.SealForProfiles} annotation + */ + String ORG_JISEL_SEAL_FOR_PROFILES = "org.jisel.SealForProfiles"; + /** + * Fully qualified name of the {@link org.jisel.SealForProfile.SealForProfilez} annotation + */ + String ORG_JISEL_SEAL_FOR_PROFILEZ = "org.jisel.SealForProfile.SealForProfilez"; + /** + * Fully qualified name of the {@link org.jisel.SealForProfiles.SealForProfilezz} annotation + */ + String ORG_JISEL_SEAL_FOR_PROFILEZZ = "org.jisel.SealForProfiles.SealForProfilezz"; + /** + * Fully qualified name of the {@link org.jisel.AddToProfile} annotation + */ + String ORG_JISEL_ADD_TO_PROFILE = "org.jisel.AddToProfile"; + /** + * Fully qualified name of the {@link org.jisel.AddToProfiles} annotation + */ + String ORG_JISEL_ADD_TO_PROFILES = "org.jisel.AddToProfiles"; + /** + * Fully qualified name of the {@link org.jisel.AddToProfile.AddToProfilez} annotation + */ + String ORG_JISEL_ADD_TO_PROFILEZ = "org.jisel.AddToProfile.AddToProfilez"; + /** + * Fully qualified name of the {@link org.jisel.AddToProfiles.AddToProfilezz} annotation + */ + String ORG_JISEL_ADD_TO_PROFILEZZ = "org.jisel.AddToProfiles.AddToProfilezz"; + + /** + * Default value to use for boolean returned values + */ + String DEFAULT_BOOLEAN_VALUE = "false"; + /** + * Default value to use for numeric returned values (int, long, float, double,...) + */ + String DEFAULT_NUMBER_VALUE = "0"; + /** + * Default value to use for Object returned values + */ + String DEFAULT_NULL_VALUE = "null"; + + /** + * Array of methods to exclude while pulling the list of all inherited methods of a class or interface + */ + String[] METHODS_TO_EXCLUDE = {"getClass", "wait", "notifyAll", "hashCode", "equals", "notify", "toString"}; + + /** + * Message displayed during compilation when 1 or many provided profiles are not found in the provided parent interfaces. + */ + String ADD_TO_PROFILE_REPORT_MSG = "1 or many provided profiles are not found in the provided parent interfaces. Check your profiles and/or parent interfaces names."; + /** + * Message displayed during compilation when more than 1 top-level parent sealed interfaces was encountered based on provided profiles + */ + String SEAL_FOR_PROFILE_REPORT_MSG = "More than 1 Top-Level Parent Sealed Interfaces will be generated. Check your profiles mapping."; + + /** + * "Report.txt" + */ + String JISEL_REPORT_SUFFIX = "Report.txt"; + + /** + * Header displayed above the list of the generated sealed interfaces, in the Jisel Report file + */ + String JISEL_REPORT_CREATED_SEALED_INTERFACES_HEADER = "Created sealed interfaces:"; + + /** + * Header displayed above the list of the sub-types of the generated sealed interfaces, in the Jisel Report file + */ + String JISEL_REPORT_CHILDREN_HEADER = "Children:"; + + /** + * Removes all commas from the provided string + * + * @param text contains commas as a string separator + * @return provided text with all commas removed + */ + static String removeCommaSeparator(final String text) { + return asList(text.split(COMMA_SEPARATOR)).stream().collect(joining()); + } + + /** + * Constructs a string based on the provided profile and a large interface {@link Element} instance, according to the naming convention:
+ * Sealed<ProfileName><LargeInterfaceSimpleName>

+ * + * @param profile name of the profile + * @param interfaceElement {@link Element} instance of the large interface to be segregated + * @return a string following Jisel sealed interface naming convention + */ + default String sealedInterfaceNameConvention(final String profile, final Element interfaceElement) { + var nameSuffix = removeCommaSeparator(profile).equals(interfaceElement.getSimpleName().toString()) ? EMPTY_STRING : interfaceElement.getSimpleName().toString(); + // any profile name starting w _ (final classes names) or containing a dot (classes annotated with addtoprofile) is returned as is + return removeCommaSeparator(profile).startsWith(UNDERSCORE) || profile.contains(DOT) ? removeCommaSeparator(profile) : format( + "%s%s%s", + SEALED_PREFIX, + removeCommaSeparator(profile), + nameSuffix + ); + } + + /** + * Constructs a string based on the provided profiles and a large interface {@link Element} instance, according to the naming convention:
+ * Sealed<ProfileName><LargeInterfaceSimpleName>

+ * + * @param profiles {@link List} of profiles names + * @param interfaceElement {@link Element} instance of the large interface to be segregated + * @return a List of string literals following Jisel sealed interface naming convention + */ + default List sealedInterfaceNameConventionForList(final List profiles, final Element interfaceElement) { + final UnaryOperator nameSuffix = profile -> removeCommaSeparator(profile).equals(interfaceElement.getSimpleName().toString()) ? EMPTY_STRING : interfaceElement.getSimpleName().toString(); + return profiles.stream() + .map(profile -> removeCommaSeparator(profile).startsWith(UNDERSCORE) || profile.contains(DOT) ? removeCommaSeparator(profile) : format( + "%s%s%s", + SEALED_PREFIX, + removeCommaSeparator(profile), + nameSuffix.apply(profile) + )).toList(); + } + + /** + * Constructs the java package name based on an {@link Element} instance of the large interface to be segregated. + * + * @param largeInterfaceElement {@link Element} instance of the large interface to be segregated + * @return the package name if any + */ + default Optional generatePackageName(final Element largeInterfaceElement) { + var qualifiedClassName = largeInterfaceElement.toString(); + int lastDot = qualifiedClassName.lastIndexOf('.'); + return lastDot > 0 ? Optional.of(qualifiedClassName.substring(0, lastDot)) : Optional.empty(); + } + + /** + * Replace all double occurences of whitespace (" ") into a single whitespace (" ") + * + * @param text contains double occurrences of whitespace + * @return the provided text with all double occurrences of whitespace replaced with a single occurence + */ + default String removeDoubleSpaceOccurrences(final String text) { + return text.replace(WHITESPACE + WHITESPACE, WHITESPACE); + } +} \ No newline at end of file diff --git a/src/main/java/org/jisel/generator/helpers/CodeGenerator.java b/src/main/java/org/jisel/generator/helpers/CodeGenerator.java deleted file mode 100644 index 66b0ea5..0000000 --- a/src/main/java/org/jisel/generator/helpers/CodeGenerator.java +++ /dev/null @@ -1,69 +0,0 @@ -/** - * Copyright (c) 2021 Mohamed Ashraf Bayor - *

- * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - *

- * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - *

- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ -package org.jisel.generator.helpers; - -import java.util.Map; - -/** - * Exposes contract for a CodeGenerator class to fulfill - */ -public sealed interface CodeGenerator permits JavaxGeneratedGenerator { - - // List of the parameters expected in the params Map object of the generateCode method: - - /** - * parameter name: "qualifiedClassName", expected type: String - */ - String QUALIFIED_CLASS_NAME = "qualifiedClassName"; - - /** - * parameter name: "fieldName", expected type: String - */ - String FIELD_NAME = "fieldName"; - - /** - * parameter name: "getterReturnType", expected type: String - */ - String GETTER_RETURN_TYPE = "getterReturnType"; - - /** - * parameter name: "getterAsString", expected type: String - */ - String GETTER_AS_STRING = "getterAsString"; - - /** - * parameter name: "gettersList", expected type: List<? extends javax.lang.model.element.Element>, ex:[getLastname(), getAge(), getMark(), getGrade(), getSchool()] - */ - String GETTERS_LIST = "gettersList"; - - /** - * parameter name: "gettersMap", expected type: Map<String, String>, ex: {getAge=int, getSchool=org.froporec.data1.School, getLastname=java.lang.String} - */ - String GETTERS_MAP = "gettersMap"; - - /** - * Generates piece of code requested, based on the parameters provided in the params object and appends it to the provided recordClassContent param - * @param recordClassContent Stringbuilder object containing the record class code being generated - * @param params expected parameters. restricted to what is expected by the implementing class. the expected parameters names are defined as constants in the CodeGenerator interface. - */ - void generateCode(StringBuilder recordClassContent, Map params); -} \ No newline at end of file diff --git a/src/main/java/org/jisel/generator/helpers/ExtendsGenerator.java b/src/main/java/org/jisel/generator/helpers/ExtendsGenerator.java deleted file mode 100644 index 21bfa1c..0000000 --- a/src/main/java/org/jisel/generator/helpers/ExtendsGenerator.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.jisel.generator.helpers; - -public final class ExtendsGenerator { -} diff --git a/src/main/java/org/jisel/generator/helpers/MethodsGenerator.java b/src/main/java/org/jisel/generator/helpers/MethodsGenerator.java deleted file mode 100644 index be5eecb..0000000 --- a/src/main/java/org/jisel/generator/helpers/MethodsGenerator.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.jisel.generator.helpers; - -public final class MethodsGenerator { -} diff --git a/src/main/java/org/jisel/generator/helpers/PermitsGenerator.java b/src/main/java/org/jisel/generator/helpers/PermitsGenerator.java deleted file mode 100644 index 529feba..0000000 --- a/src/main/java/org/jisel/generator/helpers/PermitsGenerator.java +++ /dev/null @@ -1,4 +0,0 @@ -package org.jisel.generator.helpers; - -public final class PermitsGenerator { -} diff --git a/src/main/java/org/jisel/handlers/AddToProfileHandler.java b/src/main/java/org/jisel/handlers/AddToProfileHandler.java new file mode 100644 index 0000000..40898f4 --- /dev/null +++ b/src/main/java/org/jisel/handlers/AddToProfileHandler.java @@ -0,0 +1,135 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jisel.handlers; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.toSet; +import static java.util.stream.Stream.concat; + +/** + * Handles all elements annotated with @{@link org.jisel.AddToProfile} + */ +public final class AddToProfileHandler implements JiselAnnotationHandler { + + @Override + public Map handleAnnotatedElements(ProcessingEnvironment processingEnv, + Set allAnnotatedElements, + Map>> sealedInterfacesToGenerateByLargeInterface, + Map>> sealedInterfacesPermitsByLargeInterface) { + var annotatedClassesAndInterfaces = allAnnotatedElements.stream() + .filter(element -> !element.getClass().isEnum()) + .filter(element -> ElementKind.CLASS.equals(element.getKind()) + || ElementKind.INTERFACE.equals(element.getKind()) + || ElementKind.RECORD.equals(element.getKind())) + .collect(toSet()); + var statusReport = new HashMap(); + annotatedClassesAndInterfaces.forEach(annotatedClassOrInterface -> statusReport.put( + annotatedClassOrInterface, + processAnnotatedElement(processingEnv, annotatedClassOrInterface, sealedInterfacesToGenerateByLargeInterface, sealedInterfacesPermitsByLargeInterface) + )); + return statusReport; + } + + private String processAnnotatedElement(final ProcessingEnvironment processingEnv, + final Element annotatedClassOrInterface, + final Map>> sealedInterfacesToGenerateByLargeInterface, + final Map>> sealedInterfacesPermitsByLargeInterface) { + var statusReport = new StringBuilder(); + var addToProfileProvidedProfilesMap = buildAddToProfileProvidedProfilesMap(processingEnv, annotatedClassOrInterface); + if (addToProfileProvidedProfilesMap.isEmpty()) { + // do not process if no profiles are provided + return statusReport.toString(); + } + var profileFound = false; + var providedLargeInterfaceTypeNotFound = false; + for (var mapEntrySet : addToProfileProvidedProfilesMap.entrySet()) { + var providedLargeInterfaceQualifiedName = mapEntrySet.getKey(); + var providedProfilesForProvidedLargeInterface = mapEntrySet.getValue(); + // 1st check if the provided superinterf type exists + var providedLargeInterfaceTypeOpt = Optional.ofNullable(processingEnv.getElementUtils().getTypeElement(providedLargeInterfaceQualifiedName)); + if (providedLargeInterfaceTypeOpt.isPresent()) { + var providedLargeInterfaceElement = processingEnv.getTypeUtils().asElement(processingEnv.getElementUtils().getTypeElement(providedLargeInterfaceQualifiedName).asType()); + var annotatedMethodsByProfile = sealedInterfacesToGenerateByLargeInterface.get(providedLargeInterfaceElement); + profileFound = updateSealedInterfacesPermitsMapWithProvidedProfiles( + annotatedMethodsByProfile.keySet(), + providedLargeInterfaceElement, + annotatedClassOrInterface, + providedProfilesForProvidedLargeInterface, + sealedInterfacesPermitsByLargeInterface + ); + } else { + providedLargeInterfaceTypeNotFound = true; + } + } + if (!profileFound || providedLargeInterfaceTypeNotFound) { + statusReport.append(ADD_TO_PROFILE_REPORT_MSG); + } + return statusReport.toString(); + } + + private boolean updateSealedInterfacesPermitsMapWithProvidedProfiles(final Set largeInterfaceProfilesSet, + final Element providedLargeInterfaceElement, + final Element annotatedClassOrInterface, + final Set providedProfilesForProvidedLargeInterface, + final Map>> sealedInterfacesPermitsByLargeInterface) { + var notFoundProfiles = new HashSet(); + for (var providedProfile : providedProfilesForProvidedLargeInterface) { + if (providedProfile.isBlank()) { + // only for provided empty profiles, add to the largeInterface sealed profile permits list + sealedInterfacesPermitsByLargeInterface.get(providedLargeInterfaceElement).merge( + providedLargeInterfaceElement.getSimpleName().toString(), + asList(annotatedClassOrInterface.toString()), + (currentList, newList) -> concat(currentList.stream(), newList.stream()).toList() + ); + continue; + } + var foundProvidedProfile = false; + for (var profile : largeInterfaceProfilesSet) { + if (profile.equals(providedLargeInterfaceElement.getSimpleName().toString())) { + continue; + } + if (sealedInterfaceNameConvention(providedProfile, providedLargeInterfaceElement).equals(sealedInterfaceNameConvention(profile, providedLargeInterfaceElement))) { + sealedInterfacesPermitsByLargeInterface.get(providedLargeInterfaceElement).merge( + providedProfile, + asList(annotatedClassOrInterface.toString()), + (currentList, newList) -> concat(currentList.stream(), newList.stream()).toList() + ); + foundProvidedProfile = true; + } + } + if (!foundProvidedProfile) { + notFoundProfiles.add(providedProfile); + } + } + return notFoundProfiles.isEmpty(); + } +} \ No newline at end of file diff --git a/src/main/java/org/jisel/handlers/JiselAnnotationHandler.java b/src/main/java/org/jisel/handlers/JiselAnnotationHandler.java new file mode 100644 index 0000000..e08a5eb --- /dev/null +++ b/src/main/java/org/jisel/handlers/JiselAnnotationHandler.java @@ -0,0 +1,408 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jisel.handlers; + +import org.jisel.generator.StringGenerator; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Comparator; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.regex.Pattern; + +import static java.util.Arrays.asList; +import static java.util.stream.Collectors.joining; +import static java.util.stream.Collectors.toMap; +import static java.util.stream.Collectors.toSet; +import static java.util.stream.Stream.concat; +import static org.jisel.generator.StringGenerator.removeCommaSeparator; + +/** + * Exposes contract to fulfill by any class handling all elements annotated with @{@link org.jisel.SealForProfile}(s) and @{@link org.jisel.AddToProfile}(s) annotations + */ +public sealed interface JiselAnnotationHandler extends StringGenerator permits SealForProfileHandler, AddToProfileHandler, + AnnotationInfoCollectionHandler, UniqueParentInterfaceHandler, ParentChildInheritanceHandler { + + /** + * Reads values of all attributes provided through the use of @{@link org.jisel.SealForProfile} and @{@link org.jisel.AddToProfile} annotations and + * populates the provided Map arguments + * + * @param processingEnv {@link ProcessingEnvironment} object, needed to access low-level information regarding the used annotations + * @param allAnnotatedElements {@link Set} of {@link Element} instances representing all classes annotated with @AddToProfile and + * all abstract methods annotated with @SealForProfile + * @param sealedInterfacesToGenerateByLargeInterface Map containing information about the sealed interfaces to be generated. + * To be populated and/or modified if needed. The key represents the Element instance of + * each one of the large interfaces to be segregated, while the associated value is + * a Map of profile name as the key and a Set of Element instances as the value. + * The Element instances represent each one of the abstract methods to be + * added to the generated sealed interface corresponding to a profile. + * @param sealedInterfacesPermitsByLargeInterface Map containing information about the subtypes permitted by each one of the sealed interfaces to be generated. + * To be populated and/or modified if needed. The key represents the Element instance of + * each one of the large interfaces to be segregated, while the associated value is + * a Map of profile name as the key and a List of profiles names as the value. + * @return a status report as a string value for each one of the large interfaces to be segregated + */ + Map handleAnnotatedElements(ProcessingEnvironment processingEnv, + Set allAnnotatedElements, + Map>> sealedInterfacesToGenerateByLargeInterface, + Map>> sealedInterfacesPermitsByLargeInterface); + + /** + * For a specified class or interface annotated with @{@link org.jisel.AddToProfile}, constructs a Map storing a Set of all the provided + * profiles names (as the Map value) for each one of the large interfaces names (as the Map key) provided through @AddToProfile. + * + * @param processingEnv {@link ProcessingEnvironment} object, needed to access low-level information regarding the used annotations + * @param annotatedClassOrInterface {@link Element} instance representing the annotated class or interface + * @return a Map storing a Set of all the provided profiles names (as the Map value) for each one of the large interfaces names (as the Map key) + */ + default Map> buildAddToProfileProvidedProfilesMap(final ProcessingEnvironment processingEnv, final Element annotatedClassOrInterface) { + var providedProfilesMap = new HashMap>(); + var annotationRawValueAsString = processingEnv.getElementUtils().getAllAnnotationMirrors(annotatedClassOrInterface).stream() + .map(Object::toString) + .collect(joining(COMMA_SEPARATOR)); + // sample values for annotationRawValueAsString: + // @org.jisel.AddToProfiles(profiles={"Student", "Worker"}, largeInterface="com.bayor.jisel.annotation.client.data.Sociable"),@org.jisel.AddToProfile(largeInterface="com.bayor.jisel.annotation.client.data.Sociable") + // process addToProfile + var addToProfileMatcher = Pattern.compile(ADD_TO_PROFILE_REGEX).matcher(annotationRawValueAsString); + while (addToProfileMatcher.find()) { + var attributesWithValues = addToProfileMatcher.group(1).strip(); // profile="ActiveWorker", largeInterface="com.bayor.jisel.annotation.client.data.Sociable" + var profilesSet = new HashSet(); + var profileAttributeMatcher = Pattern.compile(PROFILE_ATTRIBUTE_REGEX).matcher(attributesWithValues); + if (profileAttributeMatcher.find()) { // get only the first occurence + profilesSet.add(profileAttributeMatcher.group(1).strip()); + } + updateProvidedProfilesMapBasedOnProfilesSet(providedProfilesMap, profilesSet, attributesWithValues); + } + // process addToProfiles + var addToProfilesMatcher = Pattern.compile(ADD_TO_PROFILES_REGEX).matcher(annotationRawValueAsString); + while (addToProfilesMatcher.find()) { + var attributesWithValues = addToProfilesMatcher.group(1).strip(); + // sample values for annotationRawValueAsString: + // profiles={"Student", "Worker"}, largeInterface="com.bayor.jisel.annotation.client.data.Sociable") + var commaSeparatedProfiles = attributesWithValues.substring(attributesWithValues.indexOf(OPENING_BRACKET) + 1, attributesWithValues.indexOf(CLOSING_BRACKET)); + var profilesSet = new HashSet(); + var profilesNamesMatcher = Pattern.compile(ANNOTATION_VALUES_REGEX).matcher(commaSeparatedProfiles); + while (profilesNamesMatcher.find()) { + profilesSet.add(profilesNamesMatcher.group(1).strip()); + } + updateProvidedProfilesMapBasedOnProfilesSet(providedProfilesMap, profilesSet, attributesWithValues); + } + return providedProfilesMap; + } + + private void updateProvidedProfilesMapBasedOnProfilesSet(final Map> providedProfilesMap, final Set profilesSet, final String attributesWithValues) { + var largeInterface = EMPTY_STRING; + var largeInterfaceAttributeMatcher = Pattern.compile(LARGE_INTERFACE_ATTRIBUTE_REGEX).matcher(attributesWithValues); + if (largeInterfaceAttributeMatcher.find()) { // get only the first occurence + largeInterface = largeInterfaceAttributeMatcher.group(1).strip(); + } + providedProfilesMap.merge(largeInterface, profilesSet.isEmpty() ? new HashSet<>(Set.of(EMPTY_STRING)) : profilesSet, (currentSet, newSet) -> concat(currentSet.stream(), newSet.stream()).collect(toSet())); + } + + /** + * For a specified large interface abstract method annotated with @{@link org.jisel.SealForProfile}, constructs a Set storing all the provided profiles names + * + * @param processingEnv {@link ProcessingEnvironment} object, needed to access low-level information regarding the used annotations + * @param annotatedMethod {@link Element} instance representing the annotated method of the large interface + * @return a Set storing all the provided profiles names + */ + default Set buildSealForProfileProvidedProfilesSet(final ProcessingEnvironment processingEnv, final Element annotatedMethod) { + var providedProfilesSet = new HashSet(); + processingEnv.getElementUtils().getAllAnnotationMirrors(annotatedMethod).stream() + .flatMap(annotationMirror -> annotationMirror.getElementValues().entrySet().stream()) + .map(entry -> entry.getValue().toString()) + .forEach(annotationRawValueAsString -> { + // sample values for annotationRawValueAsString: + // single value: "profile1name" + // multiple: {@org.jisel.SealForProfile("profile2name"), @org.jisel.SealForProfile("profile3name"),...} + var matcher = Pattern.compile(ANNOTATION_VALUES_REGEX).matcher(annotationRawValueAsString); + while (matcher.find()) { + var profile = matcher.group(1).strip(); + if (profile.isBlank()) { // blank profiles ignored + continue; + } + providedProfilesSet.add(profile); + } + }); + return providedProfilesSet; + } +} + +/** + * Exposes contract to fulfill by any class dedicated to collecting necessary information from the annotated elements, + * in order to populate the {@link Map} containing the sealed interfaces information to be generated + */ +sealed interface AnnotationInfoCollectionHandler extends JiselAnnotationHandler permits SealForProfileInfoCollectionHandler { + + /** + * Populates the Map containing the sealed interfaces information to be generated + * + * @param processingEnv {@link ProcessingEnvironment} object, needed to access low-level information regarding the used annotations + * @param allAnnotatedElements {@link Set} of {@link Element} instances representing all classes annotated with @AddToProfile and + * all abstract methods annotated with @SealForProfile + * @param sealedInterfacesToGenerateByLargeInterface Map containing information about the sealed interfaces to be generated. + * To be populated and/or modified if needed. The key represents the {@link Element} instance of + * each one of the large interfaces to be segregated, while the associated value is + * a {@link Map} of profile name as the key and a Set of {@link Element} instances as the value. + * The Element instances represent each one of the abstract methods to be + * added to the generated sealed interface corresponding to a profile. + */ + void populateSealedInterfacesMap(ProcessingEnvironment processingEnv, + Set allAnnotatedElements, + Map>> sealedInterfacesToGenerateByLargeInterface); + + /** + * Creates intermediate parent interfaces based on common methods of provided profiles, then stores the created intermediate + * parent interfaces in the Map containing the sealed interfaces information to be generated + * + * @param annotatedMethodsByProfileByLargeInterface {@link Set} of all annotated abstract methods for a specified profile + * @param sealedInterfacesToGenerateByLargeInterface {@link Map} containing information about the sealed interfaces to be generated. + * To be populated and/or modified if needed. The key represents the {@link Element} instance of + * each one of the large interfaces to be segregated, while the associated value is + * a Map of profile name as the key and a Set of Element instances as the value. + * The Element instances represent each one of the abstract methods to be + * added to the generated sealed interface corresponding to a profile. + */ + default void createParentInterfacesBasedOnCommonMethods(final Map>> annotatedMethodsByProfileByLargeInterface, + final Map>> sealedInterfacesToGenerateByLargeInterface) { + annotatedMethodsByProfileByLargeInterface.forEach((interfaceElement, annotatedMethodsByProfile) -> { + var profilesList = new ArrayList(); + var methodsSetsList = new ArrayList>(); + annotatedMethodsByProfile.forEach((profileName, methodsSet) -> { + profilesList.add(profileName); + methodsSetsList.add(methodsSet); + }); + var totalProfiles = profilesList.size(); + for (int i = 0; i < totalProfiles - 1; i++) { + var allProcessedCommonMethodsByConcatenatedProfiles = concatenateProfilesBasedOnCommonMethods(profilesList.get(i), profilesList, methodsSetsList); + // remove all commonMethodElmnts 1 by 1 and in each profile + allProcessedCommonMethodsByConcatenatedProfiles.values().stream().flatMap(Collection::stream).collect(toSet()).forEach(method -> { + methodsSetsList.forEach(methodsSets -> methodsSets.remove(method)); + annotatedMethodsByProfileByLargeInterface.get(interfaceElement).forEach((profileName, methodsSets) -> methodsSets.remove(method)); + }); + // + if (!sealedInterfacesToGenerateByLargeInterface.containsKey(interfaceElement)) { + sealedInterfacesToGenerateByLargeInterface.put(interfaceElement, new HashMap<>()); + } + sealedInterfacesToGenerateByLargeInterface.get(interfaceElement).putAll(allProcessedCommonMethodsByConcatenatedProfiles); + sealedInterfacesToGenerateByLargeInterface.get(interfaceElement).putAll(annotatedMethodsByProfileByLargeInterface.get(interfaceElement)); + } + }); + } + + private Map> concatenateProfilesBasedOnCommonMethods(final String processProfileName, final List profilesList, List> methodsSetsList) { + var allProcessedCommonMethodsByConcatenatedProfiles = new HashMap>(); + var totalProfiles = profilesList.size(); + var processProfileIndex = profilesList.indexOf(processProfileName); + for (var methodElement : methodsSetsList.get(processProfileIndex)) { + var concatenatedProfiles = new StringBuilder(processProfileName); + var found = false; + for (int j = processProfileIndex + 1; j < totalProfiles; j++) { + if (methodsSetsList.get(j).contains(methodElement)) { + concatenatedProfiles.append(COMMA_SEPARATOR).append(profilesList.get(j)); + found = true; + } + } + if (found) { + allProcessedCommonMethodsByConcatenatedProfiles.merge( + concatenatedProfiles.toString(), + new HashSet<>(Set.of(methodElement)), + (currentSet, newSet) -> concat(currentSet.stream(), newSet.stream()).collect(toSet()) + ); + } + } + return allProcessedCommonMethodsByConcatenatedProfiles; + } + + @Override + default Map handleAnnotatedElements(final ProcessingEnvironment processingEnv, + final Set allAnnotatedElements, + final Map>> sealedInterfacesToGenerateByLargeInterface, + final Map>> sealedInterfacesPermitsByLargeInterface) { + populateSealedInterfacesMap(processingEnv, allAnnotatedElements, sealedInterfacesToGenerateByLargeInterface); + return Map.of(); // returning empty map instead of null + } +} + +/** + * Exposes contract to fulfill by any class dedicated to building parent-children relations based on information provided in + * the Map containing the sealed interfaces information to be generated + */ +sealed interface ParentChildInheritanceHandler extends JiselAnnotationHandler permits SealForProfileParentChildInheritanceHandler { + + /** + * Reads information stored in the Map containing the sealed interfaces information to be generated, and populates another Map storing subtypes of the provided profiles + * + * @param sealedInterfacesToGenerateByLargeInterface {@link Map} containing information about the sealed interfaces to be generated. + * To be populated and/or modified if needed. The key represents the {@link Element} instance of + * each one of the large interfaces to be segregated, while the associated value is + * a Map of profile name as the key and a Set of Element instances as the value. + * The Element instances represent each one of the abstract methods to be + * added to the generated sealed interface corresponding to a profile. + * @param sealedInterfacesPermitsByLargeInterface Map containing information about the subtypes permitted by each one of the sealed interfaces to be generated. + * To be populated and/or modified if needed. The key represents the Element instance of + * each one of the large interfaces to be segregated, while the associated value is + * a Map of profile name as the key and a List of profiles names as the value. + * @param uniqueParentInterfaceStatusReport Map returned by a call to {@link org.jisel.handlers.UniqueParentInterfaceHandler#checkAndHandleUniqueParentInterface(java.util.Map)}, + * providing information about the presence of a unique parent interface detected based on common methods of profiles + */ + void buildInheritanceRelations(Map>> sealedInterfacesToGenerateByLargeInterface, + Map>> sealedInterfacesPermitsByLargeInterface, + Map uniqueParentInterfaceStatusReport); + + /** + * Populates a Map storing subtypes of the provided profiles + * + * @param interfaceElement large interface {@link Element} instance + * @param sealedInterfacesToGenerateByLargeInterface {@link Map} containing information about the sealed interfaces to be generated. + * To be populated and/or modified if needed. The key represents the {@link Element} instance of + * each one of the large interfaces to be segregated, while the associated value is + * a Map of profile name as the key and a Set of Element instances as the value. + * The Element instances represent each one of the abstract methods to be + * added to the generated sealed interface corresponding to a profile. + * @param sealedInterfacesPermitsByLargeInterface Map containing information about the subtypes permitted by each one of the sealed interfaces to be generated. + * To be populated and/or modified if needed. The key represents the Element instance of + * each one of the large interfaces to be segregated, while the associated value is + * a Map of profile name as the key and a List of profiles names as the value. + * @param uniqueParentInterfaceStatusReport Map returned by a call to {@link org.jisel.handlers.UniqueParentInterfaceHandler#checkAndHandleUniqueParentInterface(java.util.Map)}, + * providing information about the presence of a unique parent sealed interface detected based on common methods of profiles + */ + default void buildSealedInterfacesPermitsMap(final Element interfaceElement, + final Map>> sealedInterfacesToGenerateByLargeInterface, + final Map>> sealedInterfacesPermitsByLargeInterface, + final Map uniqueParentInterfaceStatusReport) { + sealedInterfacesPermitsByLargeInterface.get(interfaceElement).putAll( + sealedInterfacesToGenerateByLargeInterface.get(interfaceElement).keySet().stream() + .filter(profiles -> profiles.contains(COMMA_SEPARATOR)) + .collect(toMap(profiles -> profiles, profiles -> asList(profiles.split(COMMA_SEPARATOR)))) + ); + // if unique parent interface is present, the parent interface permits all, + // if there are other permits already existing then the profiles in those permits lists should be removed from parent interf permits list + if (Optional.ofNullable(uniqueParentInterfaceStatusReport).isPresent() && !uniqueParentInterfaceStatusReport.containsKey(interfaceElement)) { + var parentInterfaceSimpleName = interfaceElement.getSimpleName().toString(); + var allPermittedProfiles = sealedInterfacesPermitsByLargeInterface.get(interfaceElement).values().stream().flatMap(Collection::stream).collect(toSet()); + sealedInterfacesPermitsByLargeInterface.get(interfaceElement).put( + parentInterfaceSimpleName, + sealedInterfacesToGenerateByLargeInterface.get(interfaceElement).keySet().stream() + .filter(profile -> !parentInterfaceSimpleName.equals(profile)) + .filter(profile -> !allPermittedProfiles.contains(profile)) + .toList() + ); + } + } + + @Override + default Map handleAnnotatedElements(final ProcessingEnvironment processingEnv, + final Set allAnnotatedElements, + final Map>> sealedInterfacesToGenerateByLargeInterface, + final Map>> sealedInterfacesPermitsByLargeInterface) { + buildInheritanceRelations(sealedInterfacesToGenerateByLargeInterface, sealedInterfacesPermitsByLargeInterface, null); + return Map.of(); + } +} + +/** + * Exposes contract to fulfill by any class dedicated to checking for the presence of an unique parent interface based on + * information provided in the Map containing the sealed interfaces information to be generated + */ +sealed interface UniqueParentInterfaceHandler extends JiselAnnotationHandler permits SealForProfileUniqueParentInterfaceHandler { + + /** + * Checks for the presence of an unique parent interface based on information provided in the Map containing the sealed interfaces information to be generated, + * and updates the provided map with the parent sealed interface to be generated + * + * @param sealedInterfacesToGenerateByLargeInterface Map containing information about the sealed interfaces to be generated. + * To be populated and/or modified if needed. The key represents the Element instance of + * each one of the large interfaces to be segregated, while the associated value is + * a Map of profile name as the key and a Set of Element instances as the value. + * The Element instances represent each one of the abstract methods to be + * added to the generated sealed interface corresponding to a profile. + * @return Map providing information about the presence of an unique parent sealed interface detected based on common methods of profiles.
+ * The informational message provided is a String literal describing the absence of an unique parent sealed interface, + * for each one of the provided large interfaces to be segregated + */ + Map checkAndHandleUniqueParentInterface(Map>> sealedInterfacesToGenerateByLargeInterface); + + /** + * Checks for the presence of an unique parent interface based on information provided in the Map containing the sealed interfaces information to be generated + * + * @param sealedInterfacesToGenerateByLargeInterface Map containing information about the sealed interfaces to be generated. + * To be populated and/or modified if needed. The key represents the {@link Element} instance of + * each one of the large interfaces to be segregated, while the associated value is + * a Map of profile name as the key and a Set of Element instances as the value. + * The Element instances represent each one of the abstract methods to be + * added to the generated sealed interface corresponding to a profile. + * @return Map providing information about the presence of an unique parent sealed interface detected based on common methods of profiles.
+ * If an unique parent sealed interface is detected, the Map value is an Optional containing a comma-separated concatenation of all profiles sharing same common methods + */ + default Map> checkUniqueParentInterfacePresence(final Map>> sealedInterfacesToGenerateByLargeInterface) { + + var providedProfilesListByInterface = new HashMap>(); + sealedInterfacesToGenerateByLargeInterface.forEach( + (interfaceElement, annotatedMethodsByProfile) -> annotatedMethodsByProfile.keySet().stream() + .filter(concatenatedProfiles -> !concatenatedProfiles.contains(COMMA_SEPARATOR)).forEach( + profileName -> providedProfilesListByInterface.merge( + interfaceElement, + asList(profileName), + (currentList, newList) -> concat(currentList.stream(), newList.stream()).toList()) + ) + ); + + var uniqueParentInterfaceByInterface = new HashMap>(); + + providedProfilesListByInterface.forEach((interfaceElement, profilesList) -> { + // if only 1 key of sealedInterfacesToGenerateByLargeInterface map contains all of the profiles names and its total length = all profiles names lenghts combined -> use fat interface name for that key and move on + var totalProfilesNamesLengths = profilesList.stream().mapToInt(String::length).sum(); + var longestConcatenedProfilesStringOpt = sealedInterfacesToGenerateByLargeInterface.get(interfaceElement).keySet().stream() + .sorted(Comparator.comparingInt(String::length).reversed()).findFirst(); + if (longestConcatenedProfilesStringOpt.isPresent()) { + var foundUniqueParentInterface = false; + for (var profileName : profilesList) { + foundUniqueParentInterface = removeCommaSeparator(longestConcatenedProfilesStringOpt.get()).contains(profileName); + } + uniqueParentInterfaceByInterface.put( + interfaceElement, + foundUniqueParentInterface && removeCommaSeparator(longestConcatenedProfilesStringOpt.get()).length() == totalProfilesNamesLengths + ? Optional.of(longestConcatenedProfilesStringOpt.get()) + : Optional.empty() + ); + } + }); + + return uniqueParentInterfaceByInterface; + } + + @Override + default Map handleAnnotatedElements(final ProcessingEnvironment processingEnv, + final Set allAnnotatedElements, + final Map>> sealedInterfacesToGenerateByLargeInterface, + final Map>> sealedInterfacesPermitsByLargeInterface) { + return checkAndHandleUniqueParentInterface(sealedInterfacesToGenerateByLargeInterface); + } +} \ No newline at end of file diff --git a/src/main/java/org/jisel/handlers/SealForProfileHandler.java b/src/main/java/org/jisel/handlers/SealForProfileHandler.java new file mode 100644 index 0000000..424b911 --- /dev/null +++ b/src/main/java/org/jisel/handlers/SealForProfileHandler.java @@ -0,0 +1,172 @@ +/** + * Copyright (c) 2022 Mohamed Ashraf Bayor + *

+ * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + *

+ * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + *

+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package org.jisel.handlers; + +import javax.annotation.processing.ProcessingEnvironment; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; + +import static java.util.stream.Collectors.groupingBy; +import static java.util.stream.Collectors.toSet; +import static java.util.stream.Stream.concat; + +/** + * Handles all elements annotated with @{@link org.jisel.SealForProfile} + */ +public final class SealForProfileHandler implements JiselAnnotationHandler { + + private final AnnotationInfoCollectionHandler annotationInfoCollectionHandler; + private final UniqueParentInterfaceHandler uniqueParentInterfaceHandler; + private final ParentChildInheritanceHandler parentChildInheritanceHandler; + + /** + * SealForProfileHandler constructor. Instantiates needed instances of {@link SealForProfileInfoCollectionHandler}, + * {@link SealForProfileUniqueParentInterfaceHandler} and {@link SealForProfileParentChildInheritanceHandler} + */ + public SealForProfileHandler() { + this.annotationInfoCollectionHandler = new SealForProfileInfoCollectionHandler(); + this.uniqueParentInterfaceHandler = new SealForProfileUniqueParentInterfaceHandler(); + this.parentChildInheritanceHandler = new SealForProfileParentChildInheritanceHandler(); + } + + @Override + public Map handleAnnotatedElements(final ProcessingEnvironment processingEnv, + final Set allAnnotatedElements, + final Map>> sealedInterfacesToGenerateByLargeInterface, + final Map>> sealedInterfacesPermitsByLargeInterface) { + annotationInfoCollectionHandler.populateSealedInterfacesMap(processingEnv, allAnnotatedElements, sealedInterfacesToGenerateByLargeInterface); + var statusReport = uniqueParentInterfaceHandler.checkAndHandleUniqueParentInterface(sealedInterfacesToGenerateByLargeInterface); + parentChildInheritanceHandler.buildInheritanceRelations(sealedInterfacesToGenerateByLargeInterface, sealedInterfacesPermitsByLargeInterface, statusReport); + return statusReport; + } +} + +/** + * Collects necessary information from the annotated elements, in order to populate the Map containing the sealed + * interfaces information to be generated + */ +final class SealForProfileInfoCollectionHandler implements AnnotationInfoCollectionHandler { + + @Override + public void populateSealedInterfacesMap(final ProcessingEnvironment processingEnv, + final Set allAnnotatedElements, + final Map>> sealedInterfacesToGenerateByLargeInterface) { + var annotatedMethodsByInterface = allAnnotatedElements.stream() + .filter(element -> ElementKind.METHOD.equals(element.getKind())) + .filter(element -> ElementKind.INTERFACE.equals(element.getEnclosingElement().getKind())) + .collect(groupingBy(Element::getEnclosingElement, toSet())); + if (annotatedMethodsByInterface.isEmpty()) { + return; + } + // ... + var annotatedMethodsByProfileByInterface = new HashMap>>(); + annotatedMethodsByInterface.forEach( + (interfaceElement, annotatedMethodsElements) -> annotatedMethodsElements.forEach( + annotatedMethod -> extractProfilesAndPopulateMaps( + interfaceElement, + buildSealForProfileProvidedProfilesSet(processingEnv, annotatedMethod), + annotatedMethod, + annotatedMethodsByProfileByInterface + ) + ) + ); + createParentInterfacesBasedOnCommonMethods(annotatedMethodsByProfileByInterface, sealedInterfacesToGenerateByLargeInterface); + } + + private void extractProfilesAndPopulateMaps(final Element interfaceElement, + final Set providedProfilesSet, + final Element annotatedMethod, + final Map>> annotatedMethodsByProfileByInterface) { + providedProfilesSet.forEach(profile -> { + if (annotatedMethodsByProfileByInterface.containsKey(interfaceElement)) { + annotatedMethodsByProfileByInterface.get(interfaceElement).merge( + profile, + new HashSet<>(Set.of(annotatedMethod)), + (currentSet, newSet) -> concat(currentSet.stream(), newSet.stream()).collect(toSet()) + ); + } else { + annotatedMethodsByProfileByInterface.put(interfaceElement, new HashMap<>(Map.of(profile, new HashSet<>(Set.of(annotatedMethod))))); + } + }); + } +} + +/** + * Builds parent-children relations based on information provided in the Map containing the sealed interfaces information to be generated + */ +final class SealForProfileParentChildInheritanceHandler implements ParentChildInheritanceHandler { + @Override + public void buildInheritanceRelations(final Map>> sealedInterfacesToGenerateByLargeInterface, + final Map>> sealedInterfacesPermitsByLargeInterface, + final Map uniqueParentInterfaceStatusReport) { + sealedInterfacesToGenerateByLargeInterface.keySet().forEach(interfaceElement -> { + sealedInterfacesPermitsByLargeInterface.put(interfaceElement, new HashMap<>()); // start with initializing sealedInterfacesPermitsByLargeInterface with empty mutable maps + // promote profiles with empty methods to parent level + var allProfilesToRemove = new HashSet(); + sealedInterfacesToGenerateByLargeInterface.get(interfaceElement).keySet().forEach(concatenatedProfiles -> { + var profilesArray = concatenatedProfiles.split(COMMA_SEPARATOR); + if (profilesArray.length > 1) { + for (var profile : profilesArray) { + var profileMethodsOpt = Optional.ofNullable(sealedInterfacesToGenerateByLargeInterface.get(interfaceElement).get(profile)); + if (profileMethodsOpt.isPresent() && profileMethodsOpt.get().isEmpty()) { + sealedInterfacesToGenerateByLargeInterface.get(interfaceElement).put(profile, sealedInterfacesToGenerateByLargeInterface.get(interfaceElement).get(concatenatedProfiles)); + sealedInterfacesPermitsByLargeInterface.get(interfaceElement).put(profile, Arrays.stream(profilesArray).filter(profileName -> !profile.equals(profileName)).toList()); + allProfilesToRemove.add(concatenatedProfiles); + break; + } + } + } + }); + allProfilesToRemove.forEach(sealedInterfacesToGenerateByLargeInterface.get(interfaceElement)::remove); + // and completing building sealedInterfacesPermitsByLargeInterface map + buildSealedInterfacesPermitsMap(interfaceElement, sealedInterfacesToGenerateByLargeInterface, sealedInterfacesPermitsByLargeInterface, uniqueParentInterfaceStatusReport); + }); + } +} + +/** + * Checks for the presence of an unique parent interface based on information provided in the Map containing the sealed interfaces information to be generated + */ +final class SealForProfileUniqueParentInterfaceHandler implements UniqueParentInterfaceHandler { + @Override + public Map checkAndHandleUniqueParentInterface(final Map>> sealedInterfacesToGenerateByLargeInterface) { + var statusReport = new HashMap(); + Map> longestConcatenedProfilesStringOptByInterface = checkUniqueParentInterfacePresence(sealedInterfacesToGenerateByLargeInterface); + sealedInterfacesToGenerateByLargeInterface.keySet().forEach(interfaceElement -> { + var longestConcatenedProfilesStringOpt = longestConcatenedProfilesStringOptByInterface.get(interfaceElement); + if (longestConcatenedProfilesStringOptByInterface.containsKey(interfaceElement) && longestConcatenedProfilesStringOpt.isPresent()) { + sealedInterfacesToGenerateByLargeInterface.get(interfaceElement).put(interfaceElement.getSimpleName().toString(), sealedInterfacesToGenerateByLargeInterface.get(interfaceElement).get(longestConcatenedProfilesStringOpt.get())); + sealedInterfacesToGenerateByLargeInterface.get(interfaceElement).remove(longestConcatenedProfilesStringOpt.get()); + } else { + statusReport.put(interfaceElement, SEAL_FOR_PROFILE_REPORT_MSG); + } + }); + return statusReport; + } +} \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3c4359c..a0fcd6a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,5 +1,5 @@ -info.app.name = Jisel +info.app.name = JISEL info.app.version = 1.0 -info.app.description = Interface Segregation Library for Java 17 +info.app.description = Java Interface Segregation Library info.app.contact.name = Mohamed Ashraf Bayor info.app.contact.url = https://jisel.org/ \ No newline at end of file