diff --git a/.gitignore b/.gitignore index 349d002..39670ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,84 +1,7 @@ - -### Java template -# Compiled class file -*.class - -# Log file -*.log - -# BlueJ files -*.ctxt - -# Mobile Tools for Java (J2ME) -.mtj.tmp/ - -# Package Files # -*.jar -*.war -*.nar -*.ear -*.zip -*.tar.gz -*.rar - -# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml -hs_err_pid* -### JetBrains template -# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm -# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 - -# User-specific stuff -.idea/** - -# CMake -cmake-build-*/ - -# Mongo Explorer plugin -.idea/**/mongoSettings.xml - -# File-based project format -*.iws +target +out +.idea *.iml - -# IntelliJ -out/ - -# mpeltonen/sbt-idea plugin -.idea_modules/ - -# JIRA plugin -atlassian-ide-plugin.xml - -# Cursive Clojure plugin -.idea/replstate.xml - -# Crashlytics plugin (for Android Studio and IntelliJ) -com_crashlytics_export_strings.xml -crashlytics.properties -crashlytics-build.properties -fabric.properties - -# Editor-based Rest Client -.idea/httpRequests -### Gradle template -.gradle -/build/ - -# Ignore Gradle GUI config -gradle-app.setting - -# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) -!gradle-wrapper.jar - -# Cache of project -.gradletasknamecache - -# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 -# gradle/wrapper/gradle-wrapper.properties - -grc -build/** -*/build/** -package_class.puml -*.out -.attach_pid* \ No newline at end of file +*.ipr +mvn.out +.gradle \ No newline at end of file diff --git a/annotation-processors/annotation-processor_class.properties b/annotation-processors/annotation-processor_class.properties deleted file mode 100644 index e497fd1..0000000 --- a/annotation-processors/annotation-processor_class.properties +++ /dev/null @@ -1,2 +0,0 @@ -include.files=format.iuml -title=The PlantUML class annotation processor \ No newline at end of file diff --git a/annotation-processors/build.gradle b/annotation-processors/build.gradle deleted file mode 100644 index 5c49755..0000000 --- a/annotation-processors/build.gradle +++ /dev/null @@ -1,40 +0,0 @@ -plugins { - id 'java-library' - id 'io.franzbecker.gradle-lombok' -} - -dependencies { - api project(':annotations') - api libs.'commons-lang3' - api libs.'freemarker' - api libs.'auto-service' - api libs.'slf4j-api' - api libs.'slf4j' - compileOnly libs.'lombok' - testImplementation libs.'junit' - testImplementation libs.'assertj' - testImplementation libs.'compile-testing' -} - -task copyFormat(type: Copy) { - from file("./format.iuml") - into file("out") -} - -test { - systemProperty 'pumlgen.src.dir', './src/main/java' - systemProperty 'pumlgen.test.dir', './src/test/java' -} -tasks.test.dependsOn("copyFormat") - -publishing { - publications { - maven(MavenPublication) { - groupId = group - artifactId = 'livingdoc-annotation-processors' - version = version - - from components.java - } - } -} diff --git a/annotation-processors/format.iuml b/annotation-processors/format.iuml deleted file mode 100644 index 487b020..0000000 --- a/annotation-processors/format.iuml +++ /dev/null @@ -1,15 +0,0 @@ -skinparam class { - BackgroundColor #F0F0F0/#C0C0C0 - BorderColor grey - BackgroundColor<> #e6ffe6/#b3ffb3 - BorderColor<> green - BorderColor<> green - BorderColor<> blue - BackgroundColor<> #e6e6ff/#b3b3ff - BackgroundColor<> #ffd700/yellow - BorderColor<> orange - ArrowColor black -} - -skinParam noteBackgroundColor #FFFFAA -skinParam noteBorderColor gray diff --git a/annotation-processors/ground-vehicles_class.properties b/annotation-processors/ground-vehicles_class.properties deleted file mode 100644 index a30e1be..0000000 --- a/annotation-processors/ground-vehicles_class.properties +++ /dev/null @@ -1 +0,0 @@ -include.files=format.iuml \ No newline at end of file diff --git a/annotation-processors/package_class.properties b/annotation-processors/package_class.properties deleted file mode 100644 index a30e1be..0000000 --- a/annotation-processors/package_class.properties +++ /dev/null @@ -1 +0,0 @@ -include.files=format.iuml \ No newline at end of file diff --git a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/Optionals.java b/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/Optionals.java deleted file mode 100644 index f2fd7ad..0000000 --- a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/Optionals.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.comsysto.livingdoc.annotation.processors.plantuml; - -import lombok.experimental.UtilityClass; - -import java.util.Optional; -import java.util.stream.Stream; - -/** - * Helper methods for working with {@link Optional}. - */ -@UtilityClass -public class Optionals { - - public static Stream stream(Optional opt) { - return opt.map(Stream::of) - .orElse(Stream.empty()); - } -} diff --git a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/PlantUmlClassDiagramProcessor.java b/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/PlantUmlClassDiagramProcessor.java deleted file mode 100644 index bb112e5..0000000 --- a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/PlantUmlClassDiagramProcessor.java +++ /dev/null @@ -1,238 +0,0 @@ -package com.comsysto.livingdoc.annotation.processors.plantuml; - -import static com.comsysto.livingdoc.annotation.processors.plantuml.PlantUmlClassDiagramProcessor.KEY_ENABLED; -import static com.comsysto.livingdoc.annotation.processors.plantuml.PlantUmlClassDiagramProcessor.KEY_OUT_DIR; -import static com.comsysto.livingdoc.annotation.processors.plantuml.PlantUmlClassDiagramProcessor.KEY_SETTINGS_DIR; -import static java.util.Arrays.stream; -import static java.util.Collections.emptyList; -import static java.util.Collections.singleton; -import static java.util.stream.Collectors.groupingBy; -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; -import static javax.tools.Diagnostic.Kind.WARNING; -import static org.apache.commons.lang3.BooleanUtils.toBoolean; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlDependency; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlExecutable; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlNote; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlNotes; -import com.comsysto.livingdoc.annotation.processors.plantuml.model.ClassDiagram; -import com.comsysto.livingdoc.annotation.processors.plantuml.model.DiagramId; -import com.comsysto.livingdoc.annotation.processors.plantuml.model.TypePart; -import freemarker.cache.ClassTemplateLoader; -import freemarker.template.Configuration; -import freemarker.template.Template; -import freemarker.template.TemplateException; -import lombok.extern.slf4j.Slf4j; - -import org.apache.commons.lang3.StringUtils; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileReader; -import java.io.FileWriter; -import java.io.IOException; -import java.io.Writer; -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Optional; -import java.util.Properties; -import java.util.Set; -import javax.annotation.processing.AbstractProcessor; -import javax.annotation.processing.RoundEnvironment; -import javax.annotation.processing.SupportedAnnotationTypes; -import javax.annotation.processing.SupportedOptions; -import javax.annotation.processing.SupportedSourceVersion; -import javax.lang.model.SourceVersion; -import javax.lang.model.element.TypeElement; -import javax.tools.JavaFileObject; - -/** - * The main processor class that handles top-level annotations for UMl diagrams - * (currently only {@link PlantUmlClass}). - */ -@SuppressWarnings("unused") -@SupportedAnnotationTypes("com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass") -@SupportedOptions({KEY_SETTINGS_DIR, KEY_OUT_DIR, KEY_ENABLED}) -@SupportedSourceVersion(SourceVersion.RELEASE_8) -@Slf4j -@PlantUmlClass(diagramIds = PlantUmlClassDiagramProcessor.DIAGRAM_ID) -@PlantUmlDependency(target = "ClassDiagram", description = "generates/processes") -public class PlantUmlClassDiagramProcessor extends AbstractProcessor { - protected static final String KEY_SETTINGS_DIR = "pumlgen.settings.dir"; - protected static final String DEF_SETTINGS_DIR = "."; - protected static final String KEY_OUT_DIR = "pumlgen.out.dir"; - protected static final String KEY_ENABLED = "pumlgen.enabled"; - protected static final String DEF_ENABLED = "true"; - protected static final String DEF_OUT_DIR = "./out"; - public static final String DIAGRAM_ID = "annotation-processor"; - private final Configuration freemarkerConfiguration = new Configuration(Configuration.VERSION_2_3_23); - - private String settingsDir; - private String outDir; - - public PlantUmlClassDiagramProcessor() { - freemarkerConfiguration.setTemplateLoader(new ClassTemplateLoader(this.getClass(), "")); - } - - @Override - @PlantUmlExecutable - public boolean process(final Set annotations, final RoundEnvironment roundEnv) { - log.debug("Starting processing of PlantUML annotations."); - - settingsDir = processingEnv.getOptions().getOrDefault(KEY_SETTINGS_DIR, DEF_SETTINGS_DIR); - outDir = processingEnv.getOptions().getOrDefault(KEY_OUT_DIR, getSourcePath()); - - if (!roundEnv.errorRaised() && !roundEnv.processingOver()) { - annotations.forEach(annotation -> processAnnotation(annotation, roundEnv)); - return true; - } - return false; - } - - /** - * Process a specific annotation. - * - * @param annotation the annotation - * @param roundEnv the round environment - */ - private void processAnnotation(final TypeElement annotation, final RoundEnvironment roundEnv) { - if (isEnabled()) { - final Set annotated = roundEnv.getElementsAnnotatedWith(annotation) - .stream() - .map(TypeElement.class::cast) - .collect(toSet()); - - //noinspection SwitchStatementWithTooFewBranches - switch (annotation.getQualifiedName().toString()) { - case "com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass": - processPlantumlClassAnnotation(annotated); - break; - - // Here we could add support for additional top-level annotations - - default: - log.error( - "Unexpected annotation type: {}. Most likely there is a mismatch between the value of " - + "@SupportedAnnotationTypes and the annotation handling implemented in " - + "PlantUmlClassDiagramProcessor.processAnnotation.", - annotation.getQualifiedName().toString()); - } - } - else log.info("PlantUML class diagram processing is disabled."); - } - - /** - * Defines an annotation processing option that can be used to disable the - * processor (required to compile this very project). - * - * @return true if the processor is enabled. - */ - private boolean isEnabled() { - return toBoolean(processingEnv.getOptions().getOrDefault(KEY_ENABLED, DEF_ENABLED)); - } - - /** - * Process the {@link PlantUmlClass} annotation for all types that have this - * annotation. The processor will generate one diagram file per unique - * diagram ID found on any annotation. - * - * @param annotatedTypes the annotated types. - */ - private void processPlantumlClassAnnotation(final Set annotatedTypes) { - final Map> generatedFiles = annotatedTypes.stream() - .map(this::createDiagramPart) - .flatMap(part -> part.getDiagramIds().stream() - .map(id -> new TypePart( - singleton(id), - part.getAnnotation(), - part.getTypeElement(), - part.getNotes()))) - .collect(groupingBy(part -> part.getDiagramIds().stream().findFirst().get())); - - generatedFiles.keySet().forEach(id -> createDiagram(id, generatedFiles.get(id))); - } - - private TypePart createDiagramPart(final TypeElement annotated) { - final PlantUmlClass classAnnotation = annotated.getAnnotation(PlantUmlClass.class); - log.debug("Processing PlantUmlClass annotation on type: {}", annotated); - - return new TypePart( - stream(classAnnotation.diagramIds()).map(DiagramId::of).collect(toSet()), - classAnnotation, - annotated, - getNoteAnnotations(annotated)); - } - - private List getNoteAnnotations(final TypeElement annotated) { - return Optional.ofNullable(annotated.getAnnotation(PlantUmlNotes.class)) - .map(PlantUmlNotes::value) - .map(Arrays::asList) - .orElseGet(() -> Optional.ofNullable(annotated.getAnnotation(PlantUmlNote.class)) - .map(Collections::singletonList) - .orElse(emptyList())); - } - - private void createDiagram(final DiagramId diagramId, final List parts) { - final File outFile = getOutFile(diagramId); - final Properties settings = loadSettings(diagramId); - - log.debug("Create PlantUML diagram: {}", outFile.getAbsoluteFile()); - - try (final BufferedWriter out = new BufferedWriter(new FileWriter(outFile))) { - final Template template = freemarkerConfiguration.getTemplate("class-diagram.puml.ftl"); - template.process( - new ClassDiagram( - settings.getProperty("title", null), - stream(settings.getProperty("include.files", "").split(",")) - .filter(StringUtils::isNoneBlank) - .collect(toList()), - parts), - out); - } catch (IOException | TemplateException e) { - log.error("Failed to generate diagram: " + outFile, e); - throw new RuntimeException(e); - } - } - - private File getOutFile(final DiagramId diagramId) { - final File diagramFile = new File(outDir, diagramId.getValue() + "_class.puml"); - - //noinspection ResultOfMethodCallIgnored - diagramFile.getParentFile().mkdirs(); - return diagramFile; - } - - private String getSourcePath() { - try { - final JavaFileObject generatedObject = processingEnv.getFiler() - .createSourceFile("test_" + getClass().getSimpleName()); - final Writer writer = generatedObject.openWriter(); - final File sourcePath = new File(generatedObject.toUri().getPath()).getParentFile(); - - writer.close(); - generatedObject.delete(); - return sourcePath.getAbsolutePath(); - } catch (IOException e) { - processingEnv.getMessager().printMessage(WARNING, "Unable to determine source file path!"); - return DEF_OUT_DIR; - } - } - - private Properties loadSettings(final DiagramId id) { - final File settingsFile = new File(settingsDir, id.getValue() + "_class.properties"); - final Properties settings = new Properties(); - - try (final FileReader in = new FileReader(settingsFile)) { - log.debug("Settings file: {}", settingsFile.getAbsoluteFile()); - settings.load(in); - log.debug("Settings: \n{}", settings.toString()); - } catch (IOException e) { - log.debug("No settings file found: {}", settingsFile.getAbsoluteFile()); - } - return settings; - } -} \ No newline at end of file diff --git a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/AdditionalRelationPart.java b/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/AdditionalRelationPart.java deleted file mode 100644 index 13cf50c..0000000 --- a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/AdditionalRelationPart.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.comsysto.livingdoc.annotation.processors.plantuml.model; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlField; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlNote; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlDependency; -import com.comsysto.livingdoc.annotation.processors.plantuml.PlantUmlClassDiagramProcessor; -import lombok.Value; - -@PlantUmlClass(diagramIds = PlantUmlClassDiagramProcessor.DIAGRAM_ID) -@PlantUmlNote(body = "Models an additional dependency relation") -@Value -public class AdditionalRelationPart implements RelationPart { - - @PlantUmlField - RelationId id; - - @PlantUmlField - TypePart source; - - @PlantUmlField - PlantUmlDependency relation; -} diff --git a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/ClassDiagram.java b/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/ClassDiagram.java deleted file mode 100644 index 761ca39..0000000 --- a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/ClassDiagram.java +++ /dev/null @@ -1,187 +0,0 @@ -package com.comsysto.livingdoc.annotation.processors.plantuml.model; - -import static com.comsysto.livingdoc.annotation.processors.plantuml.model.IntrinsicRelationPart.Relation.ASSOCIATION; -import static com.comsysto.livingdoc.annotation.processors.plantuml.model.IntrinsicRelationPart.Relation.INHERITANCE; -import static com.comsysto.livingdoc.annotation.processors.plantuml.model.IntrinsicRelationPart.Relation.REALIZATION; -import static java.util.Collections.emptyList; -import static java.util.stream.Collectors.joining; -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlExecutable; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlField; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlNote; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlNote.Position; -import com.comsysto.livingdoc.annotation.processors.plantuml.PlantUmlClassDiagramProcessor; -import freemarker.template.DefaultObjectWrapper; -import lombok.Data; -import lombok.EqualsAndHashCode; - -import org.apache.commons.lang3.StringUtils; - -import java.util.EnumSet; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import javax.lang.model.element.Name; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.type.WildcardType; - -/** - * Models a class diagram. The methods of this class are manly used by the - * corresponding freemarker template (class-diagram.puml.ftl). - */ -@SuppressWarnings("unused") -@EqualsAndHashCode(callSuper = true) -@Data -@PlantUmlClass(diagramIds = PlantUmlClassDiagramProcessor.DIAGRAM_ID) -@PlantUmlNote(body = "Used as input for a freemarker template\nthat uses the provided fields and methods", - position = Position.RIGHT) -public class ClassDiagram extends DefaultObjectWrapper { - - /** - * the diagram title. - */ - @PlantUmlField(showAssociation = false) - private final String title; - - /** - * Any PlantUml files to be included. - */ - @PlantUmlField(showAssociation = false) - private final List includeFiles; - - /** - * The type parts to be rendered in this diagram. - */ - @PlantUmlField - private final List parts; - - public String getTitle() { - return Optional.ofNullable(title) - .filter(StringUtils::isNoneBlank) - .orElse(null); - } - - /** - * Get all inheritance or realization relations within this diagram. - * - * @return the inheritance relations. - */ - @PlantUmlExecutable - public List getInheritanceRelations() { - final Set whitelist = renderedTypeNames(); - - return parts.stream() - .map(TypePart::getRelations) - .flatMap(List::stream) - .filter(relation -> EnumSet.of(INHERITANCE, REALIZATION).contains(relation.getRelation())) - .filter(relation -> shouldBeRendered(relation, whitelist)) - .collect(toList()); - } - - /** - * Get all (field) associations within this diagram. - * - * @return the list of association relation parts. - */ - @PlantUmlExecutable - public List getAssociations() { - final Set whitelist = renderedTypeNames(); - - return parts.stream() - .map(TypePart::getRelations) - .flatMap(List::stream) - .filter(relation -> relation.getRelation() == ASSOCIATION) - .filter(association -> shouldBeRendered(association, whitelist)) - .collect(toList()); - } - - /** - * Get all additional relations within this diagram. - * - * @return the list of association relation parts. - */ - @PlantUmlExecutable - public List getAdditionalRelations() { - final Set whitelist = renderedTypeNames().stream() - .map(Name::toString) - .collect(toSet()); - - return parts.stream() - .map(TypePart::getAdditionalRelations) - .flatMap(List::stream) - .filter(part -> whitelist.contains(part.getRelation().target())) - .collect(toList()); - } - - /** - * Get the simple name of a type. - * - * @param typeMirror the type mirror. - * - * @return the simple name. - */ - @PlantUmlExecutable - public static String simpleTypeName(TypeMirror typeMirror) { - final List typeArguments = typeMirror.getKind() == TypeKind.DECLARED - ? ((DeclaredType) typeMirror).getTypeArguments() - : emptyList(); - switch (typeMirror.getKind()) { - case DECLARED: - return declaredTypeSimpleName((DeclaredType) typeMirror, typeArguments); - case WILDCARD: - return wildcardTypeSimpleName((WildcardType) typeMirror); - default: - return typeMirror.toString(); - } - } - - private static String declaredTypeSimpleName( - final DeclaredType typeMirror, - final List typeArguments) - { - return typeMirror.asElement().getSimpleName().toString() - + (typeArguments.isEmpty() ? "" : typeParametersString(typeArguments)); - } - - private static String wildcardTypeSimpleName(final WildcardType typeMirror) { - final TypeMirror extendsBound = typeMirror.getExtendsBound(); - final TypeMirror superBound = typeMirror.getSuperBound(); - return extendsBound != null ? String.format("? extends %s", simpleTypeName(extendsBound)) - : String.format("? super %s", simpleTypeName(superBound)); - } - - private static String typeParametersString(final List typeArguments) { - return typeArguments.stream() - .map(ClassDiagram::simpleTypeName) - .collect(joining(", ", "<", ">")); - } - - /** - * Get the names of all types to be rendered within this diagram. - * - * @return the list of type names. - */ - private Set renderedTypeNames() { - return parts.stream() - .map(part -> part.getTypeElement().getSimpleName()) - .collect(toSet()); - } - - /** - * Determine if a relation should be rendered in this diagram. - * - * @param intrinsicRelationPart the relation to be checked. - * @param whitelist the list of type names to be rendered in this diagram. - * - * @return true if the type should be rendered. - */ - private boolean shouldBeRendered(final IntrinsicRelationPart intrinsicRelationPart, final Set whitelist) { - return whitelist.stream().anyMatch(name -> name.contentEquals(intrinsicRelationPart.getRight().getSimpleName())) - && whitelist.stream().anyMatch(name -> name.contentEquals(intrinsicRelationPart.getLeft().getSimpleName())); - } -} diff --git a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/DiagramId.java b/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/DiagramId.java deleted file mode 100644 index 9239bcc..0000000 --- a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/DiagramId.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.comsysto.livingdoc.annotation.processors.plantuml.model; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; -import com.comsysto.livingdoc.annotation.processors.plantuml.PlantUmlClassDiagramProcessor; -import lombok.NonNull; -import lombok.Value; - -/** - * The unique ID of a diagram. - */ -@Value -@PlantUmlClass(diagramIds = PlantUmlClassDiagramProcessor.DIAGRAM_ID) -public class DiagramId { - private static final DiagramId DEFAULT = new DiagramId("package"); - private final String value; - - private DiagramId(final String value) { - this.value = value; - } - - public static DiagramId of(@NonNull final String s) { - return s.equals(DEFAULT.value) ? DEFAULT : new DiagramId(s); - } -} diff --git a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/IntrinsicRelationPart.java b/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/IntrinsicRelationPart.java deleted file mode 100644 index cfd7dac..0000000 --- a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/IntrinsicRelationPart.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.comsysto.livingdoc.annotation.processors.plantuml.model; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlField; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlNote; -import com.comsysto.livingdoc.annotation.processors.plantuml.PlantUmlClassDiagramProcessor; -import lombok.Value; - -import javax.lang.model.element.TypeElement; - -/** - * A diagram part that represents a relation between two type elements. - */ -@Value -@PlantUmlClass(diagramIds = PlantUmlClassDiagramProcessor.DIAGRAM_ID) -@PlantUmlNote(body = "Models an inheritance relation or field association") -public class IntrinsicRelationPart implements RelationPart { - - - @PlantUmlField - RelationId id; - - @PlantUmlField - TypeElement left; - - @PlantUmlField - TypeElement right; - - @PlantUmlField - Relation relation; - - @PlantUmlClass(diagramIds = PlantUmlClassDiagramProcessor.DIAGRAM_ID) - public enum Relation { - - /** - * The right type realizes (implements) the left type. - */ - @PlantUmlField(showAssociation = false) - REALIZATION, - - /** - * The right type extends the left type. - */ - @PlantUmlField(showAssociation = false) - INHERITANCE, - - /** - * The left type references the right type in form of a directed - * association. - */ - @PlantUmlField(showAssociation = false) - ASSOCIATION - } -} diff --git a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/RelationId.java b/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/RelationId.java deleted file mode 100644 index 80a7d4d..0000000 --- a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/RelationId.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.comsysto.livingdoc.annotation.processors.plantuml.model; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; -import com.comsysto.livingdoc.annotation.processors.plantuml.PlantUmlClassDiagramProcessor; -import lombok.Value; - -/** - * Used to identify a specific relation. - */ -@Value -@PlantUmlClass(diagramIds = PlantUmlClassDiagramProcessor.DIAGRAM_ID) -public class RelationId { - String value; -} diff --git a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/RelationPart.java b/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/RelationPart.java deleted file mode 100644 index 2086404..0000000 --- a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/RelationPart.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.comsysto.livingdoc.annotation.processors.plantuml.model; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlExecutable; -import com.comsysto.livingdoc.annotation.processors.plantuml.PlantUmlClassDiagramProcessor; - -@PlantUmlClass(diagramIds = PlantUmlClassDiagramProcessor.DIAGRAM_ID) -public interface RelationPart { - - @PlantUmlExecutable - RelationId getId(); -} diff --git a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/TypePart.java b/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/TypePart.java deleted file mode 100644 index 89cc84c..0000000 --- a/annotation-processors/src/main/java/com/comsysto/livingdoc/annotation/processors/plantuml/model/TypePart.java +++ /dev/null @@ -1,250 +0,0 @@ -package com.comsysto.livingdoc.annotation.processors.plantuml.model; - -import static com.comsysto.livingdoc.annotation.processors.plantuml.Optionals.stream; -import static java.util.Collections.emptyList; -import static java.util.stream.Collectors.toList; -import static java.util.stream.Stream.concat; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlExecutable; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlField; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlNote; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlDependency; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlDependencies; -import com.comsysto.livingdoc.annotation.processors.plantuml.Optionals; -import com.comsysto.livingdoc.annotation.processors.plantuml.PlantUmlClassDiagramProcessor; -import com.comsysto.livingdoc.annotation.processors.plantuml.model.IntrinsicRelationPart.Relation; -import lombok.Value; - -import java.util.Arrays; -import java.util.Collections; -import java.util.List; -import java.util.Optional; -import java.util.Set; -import javax.lang.model.element.ElementKind; -import javax.lang.model.element.ExecutableElement; -import javax.lang.model.element.Modifier; -import javax.lang.model.element.TypeElement; -import javax.lang.model.element.VariableElement; -import javax.lang.model.type.DeclaredType; -import javax.lang.model.type.TypeKind; -import javax.lang.model.type.TypeMirror; -import javax.lang.model.type.WildcardType; - -/** - * Models a type within a class diagram. - */ -@Value -@PlantUmlClass(diagramIds = PlantUmlClassDiagramProcessor.DIAGRAM_ID) -@PlantUmlNote(body = "Models a type in the diagram") -public class TypePart { - - /** - * The IDs of the diagrams that should render this type part (please note - * this may not be equal to the diagram IDs in the annotation). - */ - @PlantUmlField - private final Set diagramIds; - - /** - * The {@link PlantUmlClass} annotation attached to the type. - */ - @PlantUmlField - private final PlantUmlClass annotation; - - /** - * The type element that models the type. - */ - @PlantUmlField - private final TypeElement typeElement; - - /** - * A list of notes associated with this type. - */ - @PlantUmlField - private final List notes; - - @PlantUmlExecutable - public String getName() { - return typeElement.getSimpleName().toString(); - } - - /** - * Get all relations independent of their type. - * - * @return all relations. - */ - public List getRelations() { - return concat( - concat( - stream(getSuperClassAssociation()), - realizationParts().stream()), - getAssociations().stream()) - .collect(toList()); - } - - /** - * Get all relations that are (field) associations. - * - * @return the associations. - */ - @PlantUmlExecutable - public List getAssociations() { - return getAnnotatedFields().stream() - .filter(element -> element.getAnnotation(PlantUmlField.class).showAssociation()) - .flatMap(element -> stream(associationPart(element))) - .collect(toList()); - } - - @PlantUmlExecutable - public List getAdditionalRelations() { - return getPlantUmlRelationAnnotations().stream() - .map(annotation -> new AdditionalRelationPart( - new RelationId(String.format("%s::%s", typeElement.getQualifiedName(), annotation.target())), - this, - annotation)) - .collect(toList()); - } - - private List getPlantUmlRelationAnnotations() { - return Optional.ofNullable(typeElement.getAnnotation(PlantUmlDependencies.class)) - .map(PlantUmlDependencies::value) - .map(Arrays::asList) - .orElseGet(() -> Optional.ofNullable(typeElement.getAnnotation(PlantUmlDependency.class)) - .map(Collections::singletonList) - .orElse(Collections.emptyList())); - } - - /** - * Get a list of variable elements representing the fields of the class that - * are annotated with {@link PlantUmlField}. - * - * @return the list of variable elements. - */ - @PlantUmlExecutable - public List getAnnotatedFields() { - return typeElement.getEnclosedElements().stream() - .filter(element -> element.getAnnotation(PlantUmlField.class) != null) - .map(VariableElement.class::cast) - .collect(toList()); - } - - /** - * Get a list of executable elements representing the methods of the class - * that are annotated with {@link PlantUmlExecutable}. - * - * @return the list of executable elements. - */ - @PlantUmlExecutable - public List getAnnotatedMethods() { - return typeElement.getEnclosedElements().stream() - .filter(element -> element.getAnnotation(PlantUmlExecutable.class) != null) - .map(ExecutableElement.class::cast) - .collect(toList()); - } - - /** - * Create the relation part for the specified variable element. - * - * @param field the variable element. - * - * @return the association relation part. - */ - public Optional associationPart(final VariableElement field) { - final TypeMirror typeMirror = field.asType(); - final List typeArguments = typeMirror.getKind() == TypeKind.DECLARED - ? ((DeclaredType) typeMirror).getTypeArguments() - : emptyList(); - return toTypeElement(typeMirror) - .map(typeElement -> new IntrinsicRelationPart( - new RelationId(field.getSimpleName().toString()), - getTypeElement(), - mainRelationType(typeElement, typeArguments), - Relation.ASSOCIATION)); - } - - private TypeElement mainRelationType( - final TypeElement typeElement, - final List typeArguments) - { - switch (typeElement.asType().getKind()) { - case DECLARED: - return typeArguments.stream().findFirst() - .flatMap(TypePart::toTypeElement) - .orElse(typeElement); - case WILDCARD: - final WildcardType wildcardType = (WildcardType) typeElement.asType(); - return toTypeElement(wildcardType.getExtendsBound() != null - ? wildcardType.getExtendsBound() - : wildcardType.getSuperBound()).orElse(typeElement); - default: - return typeElement; - } - } - - /** - * Get the relation parts for all implemented interfaces. - * - * @return the realization parts. - */ - public List realizationParts() { - return typeElement.getInterfaces().stream() - .map(TypePart::toTypeElement) - .flatMap(Optionals::stream) - .map(parentElement -> new IntrinsicRelationPart( - new RelationId(parentElement.getSimpleName().toString()), - parentElement, - getTypeElement(), - Relation.REALIZATION)) - .collect(toList()); - } - - /** - * Get the inheritance relation if there is a super class (other than - * {@link Object}). - * - * @return the inheritance relation part or an empty optional if the super - * class is {@link Object}. - */ - @PlantUmlExecutable - public Optional getSuperClassAssociation() { - return Optional.ofNullable(getTypeElement().getSuperclass()) - .filter(DeclaredType.class::isInstance) - .flatMap(TypePart::toTypeElement) - .map(parentElement -> new IntrinsicRelationPart( - new RelationId(parentElement.getSimpleName().toString()), - parentElement, - getTypeElement(), - Relation.INHERITANCE)); - } - - /** - * Converts a type mirror to a type element if it does not represent a - * primitive. - * - * @param mirror the type mirror. - * - * @return the element or an empty optional if the mirror represents a - * primitive. - */ - public static Optional toTypeElement(TypeMirror mirror) { - return mirror instanceof DeclaredType && !mirror.getKind().isPrimitive() - ? Optional.of((TypeElement) ((DeclaredType) mirror).asElement()) - : Optional.empty(); - } - - @PlantUmlExecutable - public boolean isInterface() { - return typeElement.getKind().isInterface(); - } - - @PlantUmlExecutable - public boolean isAbstract() { - return typeElement.getModifiers().contains(Modifier.ABSTRACT); - } - - @PlantUmlExecutable - public boolean isEnum() { - return typeElement.getKind() == ElementKind.ENUM; - } -} diff --git a/annotation-processors/src/main/java/freemarker_implicit.ftl b/annotation-processors/src/main/java/freemarker_implicit.ftl deleted file mode 100644 index ecd122e..0000000 --- a/annotation-processors/src/main/java/freemarker_implicit.ftl +++ /dev/null @@ -1,9 +0,0 @@ -[#ftl] -[#-- @implicitly included --] - -[#function getInheritanceRelations][/#function] -[#function geAssociations][/#function] -[#function getAdditionalRelations][/#function] -[#function getIncludeFiles][/#function] -[#function getAnnotatedFields][/#function] -[#function simpleTypeName typeMirror][/#function] \ No newline at end of file diff --git a/annotation-processors/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/annotation-processors/src/main/resources/META-INF/services/javax.annotation.processing.Processor deleted file mode 100644 index efae48e..0000000 --- a/annotation-processors/src/main/resources/META-INF/services/javax.annotation.processing.Processor +++ /dev/null @@ -1 +0,0 @@ -com.comsysto.livingdoc.annotation.processors.plantuml.PlantUmlClassDiagramProcessor \ No newline at end of file diff --git a/annotation-processors/src/main/resources/com/comsysto/livingdoc/annotation/processors/plantuml/class-diagram.puml.ftl b/annotation-processors/src/main/resources/com/comsysto/livingdoc/annotation/processors/plantuml/class-diagram.puml.ftl deleted file mode 100644 index 17288f8..0000000 --- a/annotation-processors/src/main/resources/com/comsysto/livingdoc/annotation/processors/plantuml/class-diagram.puml.ftl +++ /dev/null @@ -1,27 +0,0 @@ -<#-- @ftlvariable name="title" type="String" --> -<#-- @ftlvariable name="parts" type="java.util.List" --> -<#import "types.part.ftl" as types> -@startuml -<#list getIncludeFiles() as includeFile> -!include ${includeFile} - -hide empty members -<#if title??> -title ${title} - -<#list parts as part> - <@types.renderType part/> - - -<#list getInheritanceRelations() as association> - <@types.renderAssociation association/> - - -<#list getAssociations() as association> - <@types.renderAssociation association/> - -<#list getAdditionalRelations() as additionalRelation> - ${additionalRelation.source.name} ..> ${additionalRelation.relation.target()}: ${additionalRelation.relation.description()} - - -@enduml diff --git a/annotation-processors/src/main/resources/com/comsysto/livingdoc/annotation/processors/plantuml/types.part.ftl b/annotation-processors/src/main/resources/com/comsysto/livingdoc/annotation/processors/plantuml/types.part.ftl deleted file mode 100644 index df67d46..0000000 --- a/annotation-processors/src/main/resources/com/comsysto/livingdoc/annotation/processors/plantuml/types.part.ftl +++ /dev/null @@ -1,46 +0,0 @@ -<#macro renderType part> -<#-- @ftlvariable name="part" type="com.comsysto.livingdoc.annotation.processors.plantuml.model.TypePart" --> - <#if part.isInterface()> - <#local typeDeclaration="interface"> - <#elseif part.isAbstract()> - <#local typeDeclaration="abstract class"> - <#elseif part.isEnum()> - <#local typeDeclaration="enum"> - <#else> - <#local typeDeclaration="class"> - - <#assign fields=part.getAnnotatedFields()> - <#assign methods=part.getAnnotatedMethods()> - <#assign hasBody=fields?has_content || methods?has_content> - <#if hasBody> - - -${typeDeclaration} ${part.name}<#if hasBody> { - <#list fields as field> - ${simpleTypeName(field.asType())} ${field.simpleName} - - - <#list methods as method> - ${simpleTypeName(method.returnType)} ${method.simpleName}(<#list method.parameters as parameter>${simpleTypeName(parameter.asType())} ${parameter.simpleName}<#if parameter?has_next>, ) - -} - - - <#list part.notes as note> -note ${note.position()?lower_case} of ${part.name} -${note.body()} -end note - - - - -<#macro renderAssociation association> - <#if association.relation="INHERITANCE"> - <#local relationOperator="<|--"> - <#elseif association.relation="REALIZATION"> - <#local relationOperator="<|.."> - <#else> - <#local relationOperator="-->"> - -${association.left.simpleName} ${relationOperator} ${association.right.simpleName} - \ No newline at end of file diff --git a/annotation-processors/src/main/resources/logback.xml b/annotation-processors/src/main/resources/logback.xml deleted file mode 100644 index f24c10e..0000000 --- a/annotation-processors/src/main/resources/logback.xml +++ /dev/null @@ -1,28 +0,0 @@ - - - - - - %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n - - - - - /home/fsc/tmp/plantuml-annotation-processor.log - true - - true - - - %-4relative [%thread] %-5level %logger{35} - %msg%n - - - - - - - - - - diff --git a/annotation-processors/src/test/java/com/comsysto/livingdoc/annotation/processors/plantuml/PlantUmlClassDiagramProcessorDocGeneratorTest.java b/annotation-processors/src/test/java/com/comsysto/livingdoc/annotation/processors/plantuml/PlantUmlClassDiagramProcessorDocGeneratorTest.java deleted file mode 100644 index 167b16c..0000000 --- a/annotation-processors/src/test/java/com/comsysto/livingdoc/annotation/processors/plantuml/PlantUmlClassDiagramProcessorDocGeneratorTest.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.comsysto.livingdoc.annotation.processors.plantuml; - -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toList; - -import com.google.testing.compile.Compiler; -import com.google.testing.compile.JavaFileObjects; -import lombok.SneakyThrows; - -import org.junit.Before; -import org.junit.BeforeClass; -import org.junit.Test; - -import java.io.File; -import java.util.Optional; -import java.util.stream.Stream; -import javax.tools.JavaFileObject; - -@SuppressWarnings("ConstantConditions") -public class PlantUmlClassDiagramProcessorDocGeneratorTest { - private static final String SRC_DIR = "pumlgen.src.dir"; - private Optional srcRoot; - - @Before - public void setUp() { - srcRoot = Optional.ofNullable(System.getProperty(SRC_DIR)) - .map(File::new); - } - - @Test - @SneakyThrows - public void should_run_successfully_and_produce_expected_output() { - srcRoot.ifPresent(dir -> { - - //noinspection ResultOfMethodCallIgnored - Compiler.javac() - .withProcessors(new PlantUmlClassDiagramProcessor()) - .withOptions("-Apumlgen.out.dir=out") - .compile(streamFiles(dir) - .filter(file -> file.getName().endsWith(".java")) - .map(this::javaFileObject) - .collect(toList())); - }); - } - - private Stream streamFiles(final File dir) { - return stream(dir.listFiles()).flatMap(file -> file.isDirectory() ? streamFiles(file) : Stream.of(file)); - } - - @SneakyThrows - private JavaFileObject javaFileObject(final File f) { - return JavaFileObjects.forResource(f.toURI().toURL()); - } -} \ No newline at end of file diff --git a/annotation-processors/src/test/java/com/comsysto/livingdoc/annotation/processors/plantuml/PlantUmlClassDiagramProcessorTest.java b/annotation-processors/src/test/java/com/comsysto/livingdoc/annotation/processors/plantuml/PlantUmlClassDiagramProcessorTest.java deleted file mode 100644 index eb64c6d..0000000 --- a/annotation-processors/src/test/java/com/comsysto/livingdoc/annotation/processors/plantuml/PlantUmlClassDiagramProcessorTest.java +++ /dev/null @@ -1,85 +0,0 @@ -package com.comsysto.livingdoc.annotation.processors.plantuml; - -import static com.comsysto.livingdoc.annotation.processors.plantuml.PlantUmlClassDiagramProcessor.KEY_OUT_DIR; -import static java.util.Arrays.stream; -import static java.util.stream.Collectors.toList; -import static java.util.stream.Collectors.toSet; -import static org.assertj.core.api.Assertions.assertThat; - -import com.google.testing.compile.Compilation; -import com.google.testing.compile.Compilation.Status; -import com.google.testing.compile.Compiler; -import com.google.testing.compile.JavaFileObjects; -import lombok.SneakyThrows; - -import org.junit.Before; -import org.junit.Test; - -import java.io.File; -import java.nio.file.Files; -import java.nio.file.Paths; -import java.util.Optional; -import java.util.Set; -import javax.tools.JavaFileObject; - -@SuppressWarnings("ConstantConditions") -public class PlantUmlClassDiagramProcessorTest { - - protected static final String TEST_DIR = "pumlgen.test.dir"; - private File exampleDir; - - @Before - public void setUp() { - final File testRoot = Optional.ofNullable(System.getProperty(TEST_DIR)) - .map(File::new) - .orElseGet(this::getTestClassLoaderRoot); - exampleDir = new File(testRoot, "com/comsysto/livingdoc/example"); - } - - @SneakyThrows - private File getTestClassLoaderRoot() { - final File projectRoot = Paths.get(this.getClass().getClassLoader().getResource(".").toURI()) - .getParent() - .getParent() - .getParent() - .toFile(); - return new File(projectRoot, "src/test/java"); - } - - @Test - @SneakyThrows - public void should_run_successfully_and_produce_expected_output() { - final Compilation result = Compiler.javac() - .withProcessors(new PlantUmlClassDiagramProcessor()) - .withOptions("-Apumlgen.out.dir=out") - .compile(stream(exampleDir.listFiles()) - .map(this::javaFileObject) - .collect(toList())); - - assertThat(result.status()).isEqualTo(Status.SUCCESS); - - // Check that the file exists and contains a selected sample of expected - // lines: - assertThat(new File(System.getProperty(KEY_OUT_DIR, "out"), "package_class.puml")) - .exists() - .satisfies(file -> assertThat(lines(file)) - .contains( - "class Car", - "Wing leftWing", - "Flying <|.. FlyingVehicle", - "FlyingVehicle <|-- Airplane", - "Airplane --> Wing")); - } - - @SneakyThrows - private Set lines(final File file) { - return Files.lines(file.toPath()) - .map(String::trim) - .collect(toSet()); - } - - @SneakyThrows - private JavaFileObject javaFileObject(final File f) { - return JavaFileObjects.forResource(f.toURI().toURL()); - } -} \ No newline at end of file diff --git a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Airplane.java b/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Airplane.java deleted file mode 100644 index ff8c47e..0000000 --- a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Airplane.java +++ /dev/null @@ -1,20 +0,0 @@ -package com.comsysto.livingdoc.example; - -import static com.comsysto.livingdoc.annotation.plantuml.PlantUmlNote.Position.BOTTOM; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlField; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlNote; - -@PlantUmlClass -@PlantUmlNote( - body = "This models an airplane, a //flying// \nvehicle that is **very** fast", - position = BOTTOM) -public class Airplane extends FlyingVehicle { - - @PlantUmlField - Wing leftWing; - - @PlantUmlField - Wing rightWing; -} diff --git a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Flying.java b/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Flying.java deleted file mode 100644 index 9ece876..0000000 --- a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Flying.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.comsysto.livingdoc.example; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; - -@PlantUmlClass -public interface Flying { - - void fly(); -} diff --git a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/FlyingVehicle.java b/annotation-processors/src/test/java/com/comsysto/livingdoc/example/FlyingVehicle.java deleted file mode 100644 index 59611c6..0000000 --- a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/FlyingVehicle.java +++ /dev/null @@ -1,11 +0,0 @@ -package com.comsysto.livingdoc.example; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; - -@PlantUmlClass -public class FlyingVehicle extends Vehicle implements Flying { - - @Override - public void fly() { - } -} diff --git a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Train.java b/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Train.java deleted file mode 100644 index 29b588e..0000000 --- a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Train.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.comsysto.livingdoc.example; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlField; - -@PlantUmlClass(diagramIds = { "package", "ground-vehicles" }) -public class Train extends GroundVehicle { - - @PlantUmlField - Car loadedCar; - - public Train(final Car loadedCar, int numberOfWheels) { - this.loadedCar = loadedCar; - this.setNumberOfWheels(numberOfWheels); - } -} diff --git a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Vehicle.java b/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Vehicle.java deleted file mode 100644 index 65a9e05..0000000 --- a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Vehicle.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.comsysto.livingdoc.example; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; - -@PlantUmlClass(diagramIds = { "package", "ground-vehicles" }) -public class Vehicle { -} diff --git a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Wing.java b/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Wing.java deleted file mode 100644 index 563faef..0000000 --- a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Wing.java +++ /dev/null @@ -1,7 +0,0 @@ -package com.comsysto.livingdoc.example; - -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; - -@PlantUmlClass -public class Wing { -} diff --git a/annotations/build.gradle b/annotations/build.gradle deleted file mode 100644 index 2b32da1..0000000 --- a/annotations/build.gradle +++ /dev/null @@ -1,16 +0,0 @@ -dependencies { - testImplementation libs.'junit' -} - -publishing { - publications { - maven(MavenPublication) { - groupId = group - artifactId = 'livingdoc-annotations' - version = version - - from components.java - } - } -} - \ No newline at end of file diff --git a/annotations/pom.xml b/annotations/pom.xml new file mode 100644 index 0000000..929c42e --- /dev/null +++ b/annotations/pom.xml @@ -0,0 +1,20 @@ + + + 4.0.0 + + + com.comsysto.livingdocs + s0t + 1.0.x-SNAPSHOT + + + s0t-annotations + 1.0.x-SNAPSHOT + + + 1.8 + 1.8 + + \ No newline at end of file diff --git a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlClass.java b/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlClass.java deleted file mode 100644 index 4611ee8..0000000 --- a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlClass.java +++ /dev/null @@ -1,17 +0,0 @@ -package com.comsysto.livingdoc.annotation.plantuml; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation to be attached to a type that should be rendered as part of one or - * more PlantUML class diagrams. - */ -@Retention(RetentionPolicy.SOURCE) -@Target({ ElementType.TYPE }) -public @interface PlantUmlClass { - - String[] diagramIds() default { "package" }; -} diff --git a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlExecutable.java b/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlExecutable.java deleted file mode 100644 index 6400837..0000000 --- a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlExecutable.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.comsysto.livingdoc.annotation.plantuml; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - - -@Retention(RetentionPolicy.SOURCE) -@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR }) -public @interface PlantUmlExecutable { -} \ No newline at end of file diff --git a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlField.java b/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlField.java deleted file mode 100644 index 678e5a8..0000000 --- a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlField.java +++ /dev/null @@ -1,28 +0,0 @@ -package com.comsysto.livingdoc.annotation.plantuml; - -import java.lang.annotation.ElementType; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; -import java.lang.annotation.Target; - -/** - * Annotation to be used on a field within a class annotated with - * {@link PlantUmlClass}. This annotation causes the field to be included in - * the class body and/or as an association to the corresponding type. - */ -@Retention(RetentionPolicy.SOURCE) -@Target({ElementType.FIELD, ElementType.METHOD}) -public @interface PlantUmlField { - - /** - * Indicates if the field should be rendered as part of the class body. - */ - boolean showField() default true; - - /** - * Indicates if an association to the corresponding type element should be - * rendered in the diagram. Please note that this is only possible for types - * that are themselves part of the diagram. - */ - boolean showAssociation() default true; -} diff --git a/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/AutoCreateType.java b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/AutoCreateType.java new file mode 100644 index 0000000..f94eb6f --- /dev/null +++ b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/AutoCreateType.java @@ -0,0 +1,10 @@ +package com.comsysto.livingdoc.s0t.annotation.plantuml; + +/** + * Used to configure auto-creation preferences for a diagram element. If the + * value is set to DEFAULT, the setting is determined from the processor + * configuration. + */ +public enum AutoCreateType { + DEFAULT, YES, NO +} diff --git a/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlClass.java b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlClass.java new file mode 100644 index 0000000..b2e6db0 --- /dev/null +++ b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlClass.java @@ -0,0 +1,40 @@ +package com.comsysto.livingdoc.s0t.annotation.plantuml; + +import static com.comsysto.livingdoc.s0t.annotation.plantuml.AutoCreateType.DEFAULT; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to be attached to a type that should be rendered as part of one or + * more PlantUML class diagrams. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ ElementType.TYPE }) +public @interface PlantUmlClass { + + String[] diagramIds() default { "package" }; + + /** + * A flag that tells the annotation processor if all fields should be added + * to the class model automatically. + * + * @return YES if fields are to be added automatically or + * NO if only fields with a {@link PlantUmlField} annotation + * shall be added. + */ + AutoCreateType autoCreateFields() default DEFAULT; + + /** + * A flag that tells the annotation processor if field associations should + * be added to the class model automatically. + * + * @return YES if field associations are to be added + * automatically or NO if only fields with a + * {@link PlantUmlField} annotation shall be added. + */ + AutoCreateType autoCreateAssociations() default DEFAULT; + +} diff --git a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlDependencies.java b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlDependencies.java similarity index 84% rename from annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlDependencies.java rename to annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlDependencies.java index eea35b9..5f376f7 100644 --- a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlDependencies.java +++ b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlDependencies.java @@ -1,4 +1,4 @@ -package com.comsysto.livingdoc.annotation.plantuml; +package com.comsysto.livingdoc.s0t.annotation.plantuml; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlDependency.java b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlDependency.java similarity index 85% rename from annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlDependency.java rename to annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlDependency.java index a1a9a95..15e9fc3 100644 --- a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlDependency.java +++ b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlDependency.java @@ -1,4 +1,4 @@ -package com.comsysto.livingdoc.annotation.plantuml; +package com.comsysto.livingdoc.s0t.annotation.plantuml; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlExecutable.java b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlExecutable.java new file mode 100644 index 0000000..f526a68 --- /dev/null +++ b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlExecutable.java @@ -0,0 +1,19 @@ +package com.comsysto.livingdoc.s0t.annotation.plantuml; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + + +@Retention(RetentionPolicy.RUNTIME) +@Target({ ElementType.METHOD, ElementType.CONSTRUCTOR }) +public @interface PlantUmlExecutable { + + String[] diagramIds() default { "package" }; + + @Retention(RetentionPolicy.RUNTIME) + @Target({ ElementType.METHOD, ElementType.CONSTRUCTOR }) + @interface StartOfSequence { + } +} \ No newline at end of file diff --git a/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlField.java b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlField.java new file mode 100644 index 0000000..bd6e2f0 --- /dev/null +++ b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlField.java @@ -0,0 +1,59 @@ +package com.comsysto.livingdoc.s0t.annotation.plantuml; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * Annotation to be used on a field within a class annotated with + * {@link PlantUmlClass}. This annotation causes the field to be included in + * the class body and/or as an association to the corresponding type. + */ +@Retention(RetentionPolicy.SOURCE) +@Target({ElementType.FIELD, ElementType.METHOD}) +public @interface PlantUmlField { + + /** + * Indicates if the field should be rendered as part of the class body. + */ + boolean showField() default true; + + /** + * Indicates if an association to the corresponding type element should be + * rendered in the diagram. Please note that this is only possible for types + * that are themselves part of the diagram. + */ + boolean showAssociation() default true; + + /** + * Only relevant for a parametrized target type. In this case, this flag may + * be set to true to force the association to be handled as a standard + * association instead of a container association. + * + * @return true if the association should be a standard association. + */ + boolean forceStandardTypeAssociation() default false; + + /** + * This parameter may be used to define the association's source + * cardinality. + * + * @return the cardinality of the source side. + */ + String sourceCardinality() default ""; + + /** + * This parameter may be used to define the association's target + * cardinality. + * + * @return the cardinality of the target side. + */ + String targetCardinality() default ""; + + AssociationType associationType() default AssociationType.STANDARD; + + enum AssociationType { + STANDARD, AGGREGATION, COMPOSITION + } +} diff --git a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlNote.java b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlNote.java similarity index 88% rename from annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlNote.java rename to annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlNote.java index 773c96b..70869a4 100644 --- a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlNote.java +++ b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlNote.java @@ -1,4 +1,4 @@ -package com.comsysto.livingdoc.annotation.plantuml; +package com.comsysto.livingdoc.s0t.annotation.plantuml; import java.lang.annotation.ElementType; import java.lang.annotation.Repeatable; @@ -27,9 +27,5 @@ */ Position position() default Position.TOP; - enum Position { - TOP, BOTTOM, LEFT, RIGHT; - } - } diff --git a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlNotes.java b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlNotes.java similarity index 84% rename from annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlNotes.java rename to annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlNotes.java index 326c758..9ccbb4d 100644 --- a/annotations/src/main/java/com/comsysto/livingdoc/annotation/plantuml/PlantUmlNotes.java +++ b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/PlantUmlNotes.java @@ -1,4 +1,4 @@ -package com.comsysto.livingdoc.annotation.plantuml; +package com.comsysto.livingdoc.s0t.annotation.plantuml; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; diff --git a/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/Position.java b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/Position.java new file mode 100644 index 0000000..520ffd9 --- /dev/null +++ b/annotations/src/main/java/com/comsysto/livingdoc/s0t/annotation/plantuml/Position.java @@ -0,0 +1,5 @@ +package com.comsysto.livingdoc.s0t.annotation.plantuml; + +public enum Position { + TOP, BOTTOM, LEFT, RIGHT +} diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 91a1ea2..0000000 --- a/build.gradle +++ /dev/null @@ -1,37 +0,0 @@ -plugins { - id 'de.gafertp.plantuml' version '1.1.0' - id 'io.franzbecker.gradle-lombok' version '2.0' -} - -allprojects { - apply plugin: 'java' - apply plugin: 'maven-publish' - - sourceCompatibility = 1.8 - group = 'com.comsysto.livingdoc' - version = '1.0.x-SNAPSHOT' - - repositories { - jcenter() - mavenLocal() - } -} - -lombok { - version = '1.18.4' - sha256 = "" -} - -ext { - libs = [ - 'commons-lang3' : 'org.apache.commons:commons-lang3:3.9', - 'auto-service' : 'com.google.auto.service:auto-service:1.0-rc4', - 'freemarker' : 'org.freemarker:freemarker:2.3.23', - 'slf4j-api' : 'org.slf4j:slf4j-api:1.7.25', - 'slf4j' : 'ch.qos.logback:logback-classic:1.2.3', - 'lombok' : 'org.projectlombok:lombok:1.18.4', - 'junit' : 'junit:junit:4.12', - 'compile-testing': 'com.google.testing.compile:compile-testing:0.15', - 'assertj' : 'org.assertj:assertj-core:3.11.1' - ] -} \ No newline at end of file diff --git a/format.iuml b/format.iuml deleted file mode 100644 index 487b020..0000000 --- a/format.iuml +++ /dev/null @@ -1,15 +0,0 @@ -skinparam class { - BackgroundColor #F0F0F0/#C0C0C0 - BorderColor grey - BackgroundColor<> #e6ffe6/#b3ffb3 - BorderColor<> green - BorderColor<> green - BorderColor<> blue - BackgroundColor<> #e6e6ff/#b3b3ff - BackgroundColor<> #ffd700/yellow - BorderColor<> orange - ArrowColor black -} - -skinParam noteBackgroundColor #FFFFAA -skinParam noteBorderColor gray diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index 29953ea..0000000 Binary files a/gradle/wrapper/gradle-wrapper.jar and /dev/null differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index 5c1280e..0000000 --- a/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,6 +0,0 @@ -#Wed Jan 23 11:29:07 CET 2019 -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.2-all.zip diff --git a/gradlew b/gradlew deleted file mode 100755 index cccdd3d..0000000 --- a/gradlew +++ /dev/null @@ -1,172 +0,0 @@ -#!/usr/bin/env sh - -############################################################################## -## -## Gradle start up script for UN*X -## -############################################################################## - -# Attempt to set APP_HOME -# Resolve links: $0 may be a link -PRG="$0" -# Need this for relative symlinks. -while [ -h "$PRG" ] ; do - ls=`ls -ld "$PRG"` - link=`expr "$ls" : '.*-> \(.*\)$'` - if expr "$link" : '/.*' > /dev/null; then - PRG="$link" - else - PRG=`dirname "$PRG"`"/$link" - fi -done -SAVED="`pwd`" -cd "`dirname \"$PRG\"`/" >/dev/null -APP_HOME="`pwd -P`" -cd "$SAVED" >/dev/null - -APP_NAME="Gradle" -APP_BASE_NAME=`basename "$0"` - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS="" - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD="maximum" - -warn () { - echo "$*" -} - -die () { - echo - echo "$*" - echo - exit 1 -} - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "`uname`" in - CYGWIN* ) - cygwin=true - ;; - Darwin* ) - darwin=true - ;; - MINGW* ) - msys=true - ;; - NONSTOP* ) - nonstop=true - ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD="$JAVA_HOME/jre/sh/java" - else - JAVACMD="$JAVA_HOME/bin/java" - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD="java" - which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." -fi - -# Increase the maximum file descriptors if we can. -if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then - MAX_FD_LIMIT=`ulimit -H -n` - if [ $? -eq 0 ] ; then - if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then - MAX_FD="$MAX_FD_LIMIT" - fi - ulimit -n $MAX_FD - if [ $? -ne 0 ] ; then - warn "Could not set maximum file descriptor limit: $MAX_FD" - fi - else - warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" - fi -fi - -# For Darwin, add options to specify how the application appears in the dock -if $darwin; then - GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" -fi - -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then - APP_HOME=`cygpath --path --mixed "$APP_HOME"` - CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - JAVACMD=`cygpath --unix "$JAVACMD"` - - # We build the pattern for arguments to be converted via cygpath - ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` - SEP="" - for dir in $ROOTDIRSRAW ; do - ROOTDIRS="$ROOTDIRS$SEP$dir" - SEP="|" - done - OURCYGPATTERN="(^($ROOTDIRS))" - # Add a user-defined pattern to the cygpath arguments - if [ "$GRADLE_CYGPATTERN" != "" ] ; then - OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" - fi - # Now convert the arguments - kludge to limit ourselves to /bin/sh - i=0 - for arg in "$@" ; do - CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` - CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option - - if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition - eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` - else - eval `echo args$i`="\"$arg\"" - fi - i=$((i+1)) - done - case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; - esac -fi - -# Escape application args -save () { - for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done - echo " " -} -APP_ARGS=$(save "$@") - -# Collect all arguments for the java command, following the shell quoting and substitution rules -eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" - -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - -exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat deleted file mode 100644 index f955316..0000000 --- a/gradlew.bat +++ /dev/null @@ -1,84 +0,0 @@ -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS= - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/package_class.properties b/package_class.properties deleted file mode 100644 index 4a8329d..0000000 --- a/package_class.properties +++ /dev/null @@ -1,2 +0,0 @@ -include.files=format.iuml -title=Data Access \ No newline at end of file diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..7f71379 --- /dev/null +++ b/pom.xml @@ -0,0 +1,194 @@ + + + + 4.0.0 + + com.comsysto.livingdocs + s0t + 1.0.x-SNAPSHOT + + processors + annotations + + pom + + s0t + + + UTF-8 + 1.8 + 1.8 + official + + 12.0 + 1.0-rc7 + 2.7 + 0.19 + 2.3.30 + 2.8.6 + 28.2-jre + 3.14.5 + 5.8.2 + 1.6.10 + 3.4.2 + 1.2.9 + 1.12.1 + 1.27 + 1.8.1 + 0.16.0 + + + + + com.intellij + annotations + ${annotations.version} + + + ch.qos.logback + logback-classic + ${logback-classic.version} + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + + + + com.comsysto.livingdocs + s0t-annotations + ${project.version} + + + com.comsysto.livingdocs + s0t-processors + ${project.version} + + + org.freemarker + freemarker + ${freemarker.version} + + + com.google.guava + guava + ${guava.version} + + + org.apache.commons + commons-configuration2 + ${commons-configuration2.version} + + + org.yaml + snakeyaml + ${snakeyaml.version} + + + com.google.auto.service + auto-service + ${auto-service.version} + + + org.jetbrains.kotlin + kotlin-reflect + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-stdlib + ${kotlin.version} + + + org.jetbrains.kotlin + kotlin-stdlib-jdk8 + ${kotlin.version} + + + com.google.code.gson + gson + ${gson.version} + + + com.github.javaparser + javaparser-core + ${javaparser.version} + + + com.github.javaparser + javaparser-symbol-solver-core + ${javaparser.version} + + + io.toolisticon.aptk + tools + ${tools.version} + + + org.jetbrains.kotlin + kotlin-test-junit5 + ${kotlin.version} + test + + + io.kotlintest + kotlintest-runner-junit5 + ${kotlintest-runner-junit5.version} + test + + + org.junit.jupiter + junit-jupiter + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-api + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.junit.jupiter + junit-platform-commons + ${junit-platform.version} + test + + + org.junit.jupiter + junit-platform-engine + ${junit-platform.version} + test + + + io.mockk + mockk + ${mockk.version} + test + + + io.mockk + mockk-agent-jvm + ${mockk.version} + test + + + com.google.testing.compile + compile-testing + ${compile-testing.version} + test + + + + diff --git a/processors/pom.xml b/processors/pom.xml new file mode 100644 index 0000000..13b7458 --- /dev/null +++ b/processors/pom.xml @@ -0,0 +1,213 @@ + + + + s0t + com.comsysto.livingdocs + 1.0.x-SNAPSHOT + + 4.0.0 + + s0t-processors + + + true + 1.2.17 + 8.5.6 + + + + + com.comsysto.livingdocs + s0t-annotations + + + org.jetbrains.kotlin + kotlin-reflect + + + org.jetbrains.kotlin + kotlin-stdlib + + + org.freemarker + freemarker + + + org.apache.commons + commons-configuration2 + + + com.google.auto.service + auto-service + + + com.google.code.gson + gson + + + io.kotlintest + kotlintest-runner-junit5 + test + + + junit-platform-launcher + org.junit.platform + + + junit-platform-engine + org.junit.platform + + + kotlin-stdlib-common + org.jetbrains.kotlin + + + apiguardian-api + org.apiguardian + + + + + org.jetbrains.kotlin + kotlin-test-junit5 + test + + + io.mockk + mockk + test + + + io.mockk + mockk-agent-jvm + test + + + com.google.testing.compile + compile-testing + test + + + com.google.guava + guava + + + com.github.javaparser + javaparser-core + + + com.github.javaparser + javaparser-symbol-solver-core + + + org.yaml + snakeyaml + + + io.toolisticon.aptk + tools + + + it.unimi.dsi + fastutil-core + ${fastutil-core.version} + test + + + log4j + log4j + ${log4j.version} + test + + + + + + + org.jetbrains.kotlin + kotlin-maven-plugin + ${kotlin.version} + + + compile + compile + + compile + + + + ${project.basedir}/src/main/kotlin + + + + + test-compile + test-compile + + test-compile + + + + ${project.basedir}/src/test/kotlin + ${project.basedir}/src/test/java + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + default-compile + none + + + default-testCompile + none + + + compile + compile + + compile + + + + testCompile + test-compile + + testCompile + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.2 + + + org.junit.platform + junit-platform-surefire-provider + 1.3.2 + + + + + + s0t.src.dir + ${build.testSourceDirectory} + + + s0t.out.dir + ${build.directory}/puml + + + + + + + \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/AnnotationProcessingApiExtensions.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/AnnotationProcessingApiExtensions.kt new file mode 100644 index 0000000..94c9657 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/AnnotationProcessingApiExtensions.kt @@ -0,0 +1,29 @@ +package com.comsysto.livingdoc.s0t + +import com.comsysto.livingdoc.s0t.model.TypeName +import javax.lang.model.element.TypeElement +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.TypeMirror +import kotlin.reflect.KClass + +fun TypeElement.qName() = this.qualifiedName.toString() +fun TypeElement.typeName() = TypeName.parse(this.qualifiedName.toString()) + +/** + * Converts a type mirror to a type element if it does not represent a + * primitive. + * + * @param mirror the type mirror. + * + * @return the element or an empty optional if the mirror represents a + * primitive. + */ +fun TypeMirror.asTypeElement(): TypeElement? = when (this) { + is DeclaredType -> this.asElement() as TypeElement + else -> null +} + +fun TypeMirror.asDeclaredType() = when (this) { + is DeclaredType -> this + else -> null +} diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/Constants.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/Constants.kt new file mode 100644 index 0000000..f14abd1 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/Constants.kt @@ -0,0 +1,15 @@ +package com.comsysto.livingdoc.s0t + +const val KEY_SRC_DIR = "s0t.src.dir" +const val KEY_TPL_DIR = "s0t.freemarker.template.root" +const val KEY_TPL_EXT = "s0t.freemarker.template.extension" +const val DEF_TPL_EXT = ".s0t.ftl" + +const val KEY_OUT_DIR = "s0t.out.dir" +const val DEF_OUT_DIR = "../../../target/puml" +const val CONFIG_FILE_NAME = "s0t.yaml" + + + + + diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/Environment.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/Environment.kt new file mode 100644 index 0000000..a005029 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/Environment.kt @@ -0,0 +1,112 @@ +package com.comsysto.livingdoc.s0t + +import org.apache.commons.configuration2.Configuration +import org.apache.commons.configuration2.ConfigurationUtils +import org.apache.commons.configuration2.YAMLConfiguration +import org.slf4j.LoggerFactory +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import javax.annotation.processing.ProcessingEnvironment +import javax.annotation.processing.RoundEnvironment +import javax.tools.JavaFileManager +import javax.tools.StandardLocation + +/** + * The S0T environment. + */ +data class Environment( + + /** + * The global annotation processing environment. + */ + val processingEnvironment: ProcessingEnvironment, + + /** + * The annotation processing environment for the currently running round. + */ + val roundEnvironment: RoundEnvironment) +{ + /** + * The root path containing the YAML configuration file. Unless explicitly + * set through the annotation processor option 's0t.src.dir', this will be + * the source root. + */ + val root: Path + + /** + * An Apache Commons Configuration read from the file 's0t.yaml' in the root + * directory. + */ + val configuration: Configuration + + init { + val configurationPath: Path = configFileInUserSpecifiedPath() + ?: (configFileInSourcePath() + ?: throw IllegalStateException("Failed to determine root path.")) + + configuration = YAMLConfiguration() + + if (configurationPath.toFile().exists()) { + configuration.read(Files.newBufferedReader(configurationPath)) + } + addOptionsToConfiguration() + + root = configurationPath.parent.toAbsolutePath() + } + + private fun configFileInSourcePath() = readonlyFilerPath(StandardLocation.SOURCE_PATH, "", CONFIG_FILE_NAME) + + private fun configFileInUserSpecifiedPath() = processingEnvironment.options[KEY_SRC_DIR]?.let { Paths.get(it).resolve(CONFIG_FILE_NAME) } + + private fun addOptionsToConfiguration() { + processingEnvironment.options.entries.forEach { configuration.setProperty(it.key, it.value) } + } + + /** + * Return the path of the directory containing the specified type. + * + * @param packageName the package name. + * @param simpleTypeName the simple name of the type. + * + * @return the path of the type's source file as resolved from the root + * path. + */ + fun sourcePath(packageName: String, simpleTypeName: String): Path? = + listOf(StandardLocation.SOURCE_PATH) + .map { readonlyFilerPath(it, packageName, "${simpleTypeName}.java") } + .find { it != null } + + ?: root.resolve(Paths.get("${packageName.replace(".", "/")}/${simpleTypeName}.java")) + + /** + * Return the output directory. + */ + fun outputPath() = resolveConfiguredPath(KEY_OUT_DIR, DEF_OUT_DIR) + + private fun readonlyFilerPath(location: JavaFileManager.Location, packageName: String, fileName: String): Path? { + return try { + processingEnvironment.filer.getResource(location, packageName, fileName)?.let { Paths.get(it.toUri()) } + } catch (e: Exception) { + log.debug("Could not find {}.{} in standard location {}: {}", packageName, fileName, location.name, e.message) + null + } + } + + fun resolveConfiguredPath(configurationKey: String, defaultPath: String? = null): Path? = + configuration.getString(configurationKey, defaultPath)?.let { root.resolve(Paths.get(it)) } + + override fun toString(): String { + return "Environment(\n" + + " root=$root,\n" + + " configuration=${ConfigurationUtils.toString(configuration)},\n" + + " processingEnvironment=$processingEnvironment,\n" + + " roundEnvironment=$roundEnvironment" + + ")" + } + + + companion object { + private val log = LoggerFactory.getLogger(Environment::class.java.name) + } +} \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/S0tProcessor.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/S0tProcessor.kt new file mode 100644 index 0000000..42356bd --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/S0tProcessor.kt @@ -0,0 +1,116 @@ +package com.comsysto.livingdoc.s0t + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlExecutable +import com.comsysto.livingdoc.s0t.model.ExecutableModel +import com.comsysto.livingdoc.s0t.model.S0tModel +import com.comsysto.livingdoc.s0t.model.TypeModel +import com.comsysto.livingdoc.s0t.render.DebugRenderer +import com.comsysto.livingdoc.s0t.render.FreemarkerRenderer +import com.comsysto.livingdoc.s0t.render.OutputRenderer +import com.google.auto.service.AutoService +import org.slf4j.LoggerFactory +import javax.annotation.processing.* +import javax.lang.model.SourceVersion +import javax.lang.model.element.ExecutableElement +import javax.lang.model.element.TypeElement + +/** + * The S0T annotation processor. + */ + @SupportedAnnotationTypes( + "com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass", + "com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlExecutable.StartOfSequence" +) +@SupportedOptions(KEY_OUT_DIR) +@SupportedSourceVersion(SourceVersion.RELEASE_8) +@AutoService(Processor::class) +class S0tProcessor : AbstractProcessor() { + + /** + * The (mutable) model built from the sources. + */ + internal var model: S0tModel = S0tModel() + + /** + * The list of output renderers registered with the processor. + */ + private val outputRenderers: List = listOf(DebugRenderer, FreemarkerRenderer) + + override fun process(annotations: MutableSet, roundEnv: RoundEnvironment): Boolean { + log.info("S0T called with options: {}", processingEnv.options.toString()) + if (!roundEnv.processingOver() && !roundEnv.errorRaised()) { + createEnvironment(roundEnv)?.let { + log.info("Starting S0T processing with environment: {}", it) + threadLocalEnv.set(it) + buildModel(annotations) + renderOutput() + return true + } + } + log.info("Processing finished {}.", if (roundEnv.errorRaised()) "with errors" else "successfully") + return true + } + + private fun createEnvironment(roundEnv: RoundEnvironment): Environment? { + return try { + Environment(processingEnv, roundEnv) + } catch (e: Exception) { + log.warn("Failed to start S0T. Please add s0t.yaml to your source path (e.g. src/main/java)") + log.debug("Cause exception: ", e) + null + } + } + + private fun buildModel(annotations: MutableSet) { + annotations + .sortedBy { ANNOTATION_ORDER[it::class.qualifiedName] } + .forEach { processSingleAnnotation(it) } + } + + private fun processSingleAnnotation(annotation: TypeElement) { + when (annotation.qName()) { + PlantUmlClass::class.qualifiedName -> annotatedElements(annotation) + .map { TypeModel.of(it as TypeElement) } + .forEach { model.addType(it) } + + PlantUmlExecutable.StartOfSequence::class.qualifiedName -> annotatedElements(annotation) + .map { it as ExecutableElement } + .forEach { model.addExecutable(ExecutableModel.of(it)) } + + else -> log.error( + """Unexpected annotation type: {}. Most likely there is a mismatch between the value of + @SupportedAnnotationTypes and the annotation handling implemented in processAnnotation.""", + annotation.qName() + ) + } + } + + private fun annotatedElements(annotation: TypeElement) = environment().roundEnvironment.getElementsAnnotatedWith(annotation) + + private fun renderOutput() { + outputRenderers.forEach { it.render(model) } + } + + companion object { + private val log = LoggerFactory.getLogger(S0tProcessor::class.java.name) + + /** + * Defines the order for processing annotations. This is used to make + * sure that all annotations of a specific type (e.g. PlantUmlClass) are + * processed before annotations with a lower priority, so that all model + * elements on which other model elements depend are available at the + * time of processing. + */ + val ANNOTATION_ORDER: Map = mapOf( + PlantUmlClass::class.qualifiedName!! to 0, + PlantUmlExecutable.StartOfSequence::class.qualifiedName!! to 1 + ) + + fun environment() = threadLocalEnv.get()!! + fun configuration() = environment().configuration + + private val threadLocalEnv = ThreadLocal() + } + +} \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/AccessModifier.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/AccessModifier.kt new file mode 100644 index 0000000..7924412 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/AccessModifier.kt @@ -0,0 +1,5 @@ +package com.comsysto.livingdoc.s0t.model + +enum class AccessModifier { + PUBLIC, PROTECTED, PACKAGE, PRIVATE +} \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/ExecutableModel.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/ExecutableModel.kt new file mode 100644 index 0000000..fdaa227 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/ExecutableModel.kt @@ -0,0 +1,85 @@ +package com.comsysto.livingdoc.s0t.model + +import com.comsysto.livingdoc.s0t.S0tProcessor.Companion.environment +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlExecutable +import com.comsysto.livingdoc.s0t.model.TypeName.ComplexTypeName +import com.github.javaparser.StaticJavaParser +import com.github.javaparser.ast.CompilationUnit +import com.github.javaparser.ast.body.MethodDeclaration +import com.github.javaparser.ast.expr.MethodCallExpr +import com.github.javaparser.resolution.declarations.ResolvedMethodDeclaration +import com.github.javaparser.symbolsolver.JavaSymbolSolver +import com.github.javaparser.symbolsolver.resolution.typesolvers.CombinedTypeSolver +import com.github.javaparser.symbolsolver.resolution.typesolvers.JavaParserTypeSolver +import com.github.javaparser.symbolsolver.resolution.typesolvers.ReflectionTypeSolver +import java.io.File +import java.nio.file.Paths +import java.util.* +import javax.lang.model.element.ExecutableElement +import javax.tools.StandardLocation + +/** + * Models a sequence of executable members in a tree-like structure. + */ +data class ExecutableModel(val name: ExecutableName, val outgoingCalls: List, val signature: String? = null) { + + companion object { + + /** + * Creates an instance from an ExecutableElement. This is usually only + * used for the start of a sequence (annotated with @StartOfSequence), + * as all other models are created from _JavaParser_ output. + */ + fun of(element: ExecutableElement): ExecutableModel { + val enclosingType = TypeRef.of(element.enclosingElement.asType()) + val compilationUnit = compilationUnit(enclosingType.name.packageName, enclosingType.name.simpleName) + + val name = ExecutableName(enclosingType.name as ComplexTypeName, element.simpleName.toString()) + return ExecutableModel(name, outgoingCalls(compilationUnit, name)) + } + + /** + * Creates an instance from a ResolvedMethodDeclaration returned by + * _JavaParser_. This method will be called recursively to create the + * whole sequence, resulting in a tree of executable elements. + */ + fun of(resolved: ResolvedMethodDeclaration): ExecutableModel { + val compilationUnit = compilationUnit(resolved.packageName, resolved.className) + val name = ExecutableName(ComplexTypeName(resolved.packageName, resolved.className), resolved.name) + + return ExecutableModel(name, outgoingCalls(compilationUnit, name), resolved.signature) + } + + /** + * Parse a Java class with the specified package and type named using + * _JavaParser_. + * + * @return the compilation unit + */ + private fun compilationUnit(packageName: String, simpleTypeName: String): CompilationUnit { + val filePath = environment().sourcePath(packageName, simpleTypeName)!! + val typeSolver = CombinedTypeSolver(JavaParserTypeSolver(environment().root), ReflectionTypeSolver()) + + StaticJavaParser.getConfiguration().setSymbolResolver(JavaSymbolSolver(typeSolver)) + return StaticJavaParser.parse(filePath.toFile()) + } + + /** + * Get the ExecutableModel for an executable's outgoing calls from the + * compilation unit. + */ + private fun outgoingCalls(compilationUnit: CompilationUnit, executableName: ExecutableName): List { + return methodDeclarations(compilationUnit, executableName) + .map { it.body.orElse(null) } + .flatMap { block -> block.findAll(MethodCallExpr::class.java).map { it.resolve() } } + .map { of(it) } + } + + private fun methodDeclarations(unit: CompilationUnit, executableName: ExecutableName): List { + return unit.types + .filter { type -> type.fullyQualifiedName == Optional.of(executableName.typeName.asQualifiedName()) } + .flatMap { type -> type.methods } + .filter { method -> method.getAnnotationByClass(PlantUmlExecutable::class.java).isPresent && method.nameAsString == executableName.name } + } + } +} \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/ExecutableName.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/ExecutableName.kt new file mode 100644 index 0000000..a23d4f3 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/ExecutableName.kt @@ -0,0 +1,11 @@ +package com.comsysto.livingdoc.s0t.model + +/** + * The fully-qualified name of an executable. + */ +data class ExecutableName(val typeName: TypeName.ComplexTypeName, val name: String) : Comparable { + override fun compareTo(other: ExecutableName): Int { + val typeResult = typeName.compareTo(other.typeName) + return if (typeResult != 0) typeResult else name.compareTo(other.name) + } +} \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/FieldModel.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/FieldModel.kt new file mode 100644 index 0000000..5518864 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/FieldModel.kt @@ -0,0 +1,60 @@ +package com.comsysto.livingdoc.s0t.model + +import com.comsysto.livingdoc.s0t.S0tProcessor.Companion.configuration +import com.comsysto.livingdoc.s0t.annotation.plantuml.AutoCreateType +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlField +import com.comsysto.livingdoc.s0t.asDeclaredType +import com.comsysto.livingdoc.s0t.model.AccessModifier.* +import javax.lang.model.element.Modifier +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement + +private const val KEY_AUTO_ADD_FIELDS = "s0t.plantuml.class.member.field.auto-add" +private const val DEF_AUTO_ADD_FIELDS = true + +/** + * Models a field. + */ +data class FieldModel( + val name: String, + val type: TypeRef, + val accessModifier: AccessModifier, + val typeParameters: List = emptyList() +) { + + companion object { + + fun of(field: VariableElement) = FieldModel( + field.simpleName.toString(), + TypeRef.of(field.asType()), + field.modifiers.map { asAccessModifier(it) }.find { it != null } ?: PACKAGE, + field.asType().asDeclaredType()?.let { it.typeArguments.mapNotNull { arg -> TypeRef.of(arg) } } ?: emptyList()) + + private fun asAccessModifier(modifier: Modifier): AccessModifier? = when (modifier) { + Modifier.PUBLIC -> PUBLIC + Modifier.PROTECTED -> PROTECTED + Modifier.DEFAULT -> PACKAGE + Modifier.PRIVATE -> PRIVATE + else -> null + } + + /** + * Get all the fields of a TypeElement. + */ + fun allOf(typeElement: TypeElement): List { + val autoCreate = autoCreateFields(typeElement) + + return typeElement.enclosedElements + .filterIsInstance(VariableElement::class.java) + .filter { autoCreate || it.getAnnotation(PlantUmlField::class.java) != null } + .map { of(it) } + } + + private fun autoCreateFields(typeElement: TypeElement): Boolean { + val autoCreateFields = typeElement.getAnnotation(PlantUmlClass::class.java)?.autoCreateFields ?: AutoCreateType.DEFAULT + return (autoCreateFields == AutoCreateType.YES + || autoCreateFields == AutoCreateType.DEFAULT && configuration().getBoolean(KEY_AUTO_ADD_FIELDS, DEF_AUTO_ADD_FIELDS)) + } + } +} \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/Note.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/Note.kt new file mode 100644 index 0000000..b9ee294 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/Note.kt @@ -0,0 +1,16 @@ +package com.comsysto.livingdoc.s0t.model + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlNotes +import javax.lang.model.element.TypeElement + +/** + * Models a note. + */ +data class Note(val text: String, val position: Position) { + + companion object { + fun allOf(typeElement: TypeElement): List = typeElement.getAnnotation(PlantUmlNotes::class.java) + ?.let { notes -> notes.value.map { Note(it.body, Position.valueOf(it.position.name)) } } + .orEmpty() + } +} \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/Position.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/Position.kt new file mode 100644 index 0000000..f026cbf --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/Position.kt @@ -0,0 +1,8 @@ +package com.comsysto.livingdoc.s0t.model + +/** + * The position of one diagram element relative to another. + */ +enum class Position { + TOP, BOTTOM, LEFT, RIGHT; +} diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/Relation.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/Relation.kt new file mode 100644 index 0000000..4629f1d --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/Relation.kt @@ -0,0 +1,155 @@ +package com.comsysto.livingdoc.s0t.model + +import com.comsysto.livingdoc.s0t.S0tProcessor.Companion.configuration +import com.comsysto.livingdoc.s0t.annotation.plantuml.AutoCreateType +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlField +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlField.AssociationType +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlField.AssociationType.STANDARD +import com.comsysto.livingdoc.s0t.asDeclaredType +import com.comsysto.livingdoc.s0t.asTypeElement +import javax.lang.model.element.ElementKind +import javax.lang.model.element.TypeElement +import javax.lang.model.element.VariableElement +import javax.lang.model.type.DeclaredType + +private const val KEY_AUTO_ADD_ASSOCIATIONS = "s0t.plantuml.class.relation.association.auto-add" +private const val DEF_AUTO_ADD_ASSOCIATIONS = true + +/** + * Models a relation. Known relation types are **Realization** (e.g. a type + * implementing an interface), **Inheritance** (a type inheriting from another) + * and **Association** (a type referencing another via a field). + */ +interface Relation { + + val left: TypeRef + val right: TypeRef + + abstract class BaseRelation(val id: RelationId) : Relation + + data class Realization(val interfaceType: TypeRef, val implementingType: TypeRef) : BaseRelation(RelationId(interfaceType.name.asQualifiedName())) { + override val left: TypeRef + get() = interfaceType + override val right: TypeRef + get() = implementingType + + companion object { + + /** + * Get all realizations from an annotation API TypeElement. + */ + fun allOf(typeElement: TypeElement): List = typeElement.interfaces + .filterIsInstance() + .mapNotNull { it.asTypeElement() } + .filter { it.getAnnotation(PlantUmlClass::class.java) != null } + .map { interfaceElement -> Realization(TypeRef.of(interfaceElement), TypeRef.of(typeElement)) } + } + } + + data class Inheritance(val superClassType: TypeRef, val implementingType: TypeRef) : BaseRelation(RelationId(superClassType.name.asQualifiedName())) { + override val left: TypeRef + get() = superClassType + override val right: TypeRef + get() = implementingType + + companion object { + + /** + * Get the super class from an annotation API TypeElement. + */ + fun of(typeElement: TypeElement): Inheritance? = typeElement.superclass + .asTypeElement()?.let { + if (it.qualifiedName.toString() != "java.lang.Object" && it.getAnnotation(PlantUmlClass::class.java) != null) + Inheritance(TypeRef.of(it), TypeRef.of(typeElement)) + else null + } + } + } + + data class Association( + val sourceType: TypeRef, + val targetType: TypeRef, + val field: FieldModel, + val sourceCardinality: String = "", + val targetCardinality: String = "", + val type: AssociationType = STANDARD + ) : BaseRelation(RelationId(field.name)) { + + override val left: TypeRef + get() = sourceType + override val right: TypeRef + get() = targetType + + companion object { + + /** + * Get all associations defined by the fields of an annotation API + * TypeElement. + */ + internal fun allOf(typeElement: TypeElement): List { + val autoCreate = autoCreateAssociations(typeElement) + + return typeElement.enclosedElements + .filterIsInstance(VariableElement::class.java) + .filter { it.asType().asTypeElement()?.kind != ElementKind.ENUM || !it.asType().equals(typeElement.asType()) } + .filter { (autoCreate || fieldAnnotation(it) != null && fieldAnnotation(it)?.showAssociation == DEF_AUTO_ADD_ASSOCIATIONS) } + .flatMap { allOf(it, typeElement) } + } + + private fun autoCreateAssociations(typeElement: TypeElement): Boolean { + val autoCreateAssociations = typeElement.getAnnotation(PlantUmlClass::class.java)?.autoCreateAssociations ?: AutoCreateType.DEFAULT + return (autoCreateAssociations == AutoCreateType.YES + || autoCreateAssociations == AutoCreateType.DEFAULT && configuration().getBoolean(KEY_AUTO_ADD_ASSOCIATIONS, DEF_AUTO_ADD_ASSOCIATIONS)) + } + + internal fun allOf(v: VariableElement, enclosingTypeElement: TypeElement) = + if (targetsContainerType(v)) { containerTypeAssociations(enclosingTypeElement, v) } + else standardTypeAssociation(enclosingTypeElement, v) + + private fun targetsContainerType(v: VariableElement) = + (v.asType().asDeclaredType()?.typeArguments ?: emptyList()).isNotEmpty() && !(fieldAnnotation(v)?.forceStandardTypeAssociation ?: false) + + /** + * Determines if the specified type is a container type (e.g. a + * collection or optional type) and returns associations to all its + * type parameters. + */ + private fun containerTypeAssociations(sourceTypeElement: TypeElement, v: VariableElement): List = + v.asType().asDeclaredType() + ?.let { targetTypes(it)?.map { t -> typeAssociation(sourceTypeElement, t, v, fieldAnnotation(v)) } } + ?: emptyList() + + /** + * Get the target types of a field with a container type. + */ + internal fun targetTypes(type: DeclaredType): Set? = if (type.typeArguments.isNotEmpty()) { + type.typeArguments.toSet() + .filter { it.asTypeElement()?.getAnnotation(PlantUmlClass::class.java) != null } + .filterIsInstance() + .map { TypeRef.of(it) } + .toSet() + } + else null + + /** + * Returns a standard type association for a non-container type. + */ + private fun standardTypeAssociation(sourceType: TypeElement, v: VariableElement) = + if (v.asType().asTypeElement()?.getAnnotation(PlantUmlClass::class.java) != null) + listOfNotNull(typeAssociation(sourceType, TypeRef.of(v.asType()), v, fieldAnnotation(v))) + else emptyList() + + private fun fieldAnnotation(it: VariableElement): PlantUmlField? = it.getAnnotation(PlantUmlField::class.java) + + private fun typeAssociation(sourceType: TypeElement, targetType: TypeRef, v: VariableElement, fieldAnnotation: PlantUmlField?) = Association( + TypeRef.of(sourceType), + targetType, + FieldModel.of(v), + fieldAnnotation?.sourceCardinality ?: "", + fieldAnnotation?.targetCardinality ?: "", + fieldAnnotation?.associationType ?: STANDARD + ) + } + } +} diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/RelationId.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/RelationId.kt new file mode 100644 index 0000000..76b10a5 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/RelationId.kt @@ -0,0 +1,6 @@ +package com.comsysto.livingdoc.s0t.model + +/** + * Used to identify a specific relation. + */ +data class RelationId(val value: String) \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/S0tModel.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/S0tModel.kt new file mode 100644 index 0000000..5d9af57 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/S0tModel.kt @@ -0,0 +1,22 @@ +package com.comsysto.livingdoc.s0t.model + +import com.comsysto.livingdoc.s0t.model.TypeName.ComplexTypeName +import java.util.* + +/** + * This is the root of SoT's data model. It is built step by step by all + * participating annotation processors and, at the end of processing, contains + * the accumulated results. + */ +class S0tModel { + val types = TreeMap() + val executables = TreeMap() + + fun addType(type: TypeModel) { + types[type.name] = type + } + + fun addExecutable(executable: ExecutableModel) { + executables[executable.name] = executable + } +} \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/TypeModel.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/TypeModel.kt new file mode 100644 index 0000000..373edbd --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/TypeModel.kt @@ -0,0 +1,55 @@ +package com.comsysto.livingdoc.s0t.model + +import com.comsysto.livingdoc.s0t.asTypeElement +import com.comsysto.livingdoc.s0t.model.Relation.* +import com.comsysto.livingdoc.s0t.typeName +import javax.lang.model.element.ElementKind +import javax.lang.model.element.Modifier +import javax.lang.model.element.TypeElement +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.TypeMirror +import kotlin.reflect.KClass + +/** + * Models a language type. + */ +data class TypeModel( + val name: TypeName.ComplexTypeName, + val type: Type, + val fields: List, + val realizations: List = listOf(), + val inheritance: Inheritance? = null, + val associations: List = listOf(), + val notes: List = listOf() +) { + enum class Type { + INTERFACE, ABSTRACT, CLASS, ENUM; + + companion object { + fun of(typeElement: TypeElement): Type = when { + typeElement.kind.isInterface -> INTERFACE + typeElement.modifiers.contains(Modifier.ABSTRACT) -> ABSTRACT + typeElement.kind == ElementKind.ENUM -> ENUM + else -> CLASS + } + } + } + + companion object { + + /** + * Create a type model from a TypeElement. + * + * @param typeElement an annotation API type element + * @return the type model. + */ + fun of(typeElement: TypeElement) = TypeModel( + typeElement.typeName() as TypeName.ComplexTypeName, + Type.of(typeElement), + FieldModel.allOf(typeElement), + Realization.allOf(typeElement), + Inheritance.of(typeElement), + Association.allOf(typeElement), + Note.allOf(typeElement)) + } +} \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/TypeName.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/TypeName.kt new file mode 100644 index 0000000..ae7fa46 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/TypeName.kt @@ -0,0 +1,41 @@ +package com.comsysto.livingdoc.s0t.model + +import org.apache.commons.lang3.StringUtils.substringAfterLast +import org.apache.commons.lang3.StringUtils.substringBeforeLast +import kotlin.reflect.KClass + +/** + * The name of a type. + */ +interface TypeName: Comparable { + val packageName: String + val simpleName: String + fun asQualifiedName(): String + + override fun compareTo(other: TypeName): Int { + return asQualifiedName().compareTo(other.asQualifiedName()) + } + companion object { + fun parse(fqn: String) = if (fqn.contains('.')) ComplexTypeName(substringBeforeLast(fqn, "."), substringAfterLast(fqn, ".")) + else SimpleTypeName(fqn) + } + + /** + * A simple unqualified type name. + */ + data class SimpleTypeName(val name: String): TypeName { + override val packageName: String + get() = "" + override val simpleName: String + get() = name + + override fun asQualifiedName() = name + } + + /** + * A fully qualified type name. + */ + data class ComplexTypeName (override val packageName: String, override val simpleName: String): TypeName { + override fun asQualifiedName() = "${packageName}.${simpleName}" + } +} diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/TypeRef.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/TypeRef.kt new file mode 100644 index 0000000..8ba61fa --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/model/TypeRef.kt @@ -0,0 +1,27 @@ +package com.comsysto.livingdoc.s0t.model + +import com.comsysto.livingdoc.s0t.asTypeElement +import com.comsysto.livingdoc.s0t.typeName +import javax.lang.model.element.TypeElement +import javax.lang.model.type.TypeKind.* +import javax.lang.model.type.TypeMirror + +/** + * Models the _reference_ to a type that is possibly not yet known (e.g. because + * it hasn't been visited yet by the annotation processor). + */ +data class TypeRef(val name: TypeName, val kind: Kind) { + + companion object { + fun of(type: TypeMirror): TypeRef = when(type.kind) { + BOOLEAN, BYTE, SHORT, INT, LONG, CHAR, FLOAT, DOUBLE -> TypeRef(TypeName.SimpleTypeName(type.toString()), Kind.PRIMITIVE) + DECLARED -> of(type.asTypeElement()!!) + else -> TypeRef(TypeName.SimpleTypeName(type.toString()), Kind.UNKNOWN) + } + fun of(typeElement: TypeElement): TypeRef = TypeRef(typeElement.typeName(), Kind.COMPLEX) + } + + enum class Kind { + PRIMITIVE, COMPLEX, UNKNOWN + } +} \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/render/DebugRenderer.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/render/DebugRenderer.kt new file mode 100644 index 0000000..19146e0 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/render/DebugRenderer.kt @@ -0,0 +1,14 @@ +package com.comsysto.livingdoc.s0t.render + +import com.comsysto.livingdoc.s0t.model.S0tModel +import com.google.gson.GsonBuilder +import org.slf4j.LoggerFactory + +object DebugRenderer: OutputRenderer { + private val log = LoggerFactory.getLogger(DebugRenderer::class.java.name) + private val gson = GsonBuilder().setPrettyPrinting().create() + + override fun render(model: S0tModel) { + log.debug("s0t model passed to renderers: {}", gson.toJson(model, S0tModel::class.java)) + } +} \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/render/FreemarkerRenderer.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/render/FreemarkerRenderer.kt new file mode 100644 index 0000000..cfe2a00 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/render/FreemarkerRenderer.kt @@ -0,0 +1,98 @@ +package com.comsysto.livingdoc.s0t.render + +import com.comsysto.livingdoc.s0t.model.S0tModel +import com.comsysto.livingdoc.s0t.* +import com.comsysto.livingdoc.s0t.S0tProcessor.Companion.environment +import freemarker.cache.ClassTemplateLoader +import freemarker.cache.FileTemplateLoader +import freemarker.cache.MultiTemplateLoader +import freemarker.template.Configuration +import freemarker.template.Configuration.VERSION_2_3_23 +import freemarker.template.Template +import org.apache.commons.configuration2.MapConfiguration +import org.slf4j.LoggerFactory +import java.io.File +import java.nio.charset.StandardCharsets.UTF_8 + +object FreemarkerRenderer : OutputRenderer { + private val log = LoggerFactory.getLogger(FreemarkerRenderer::class.java.name) + + private const val CUSTOM_ATTRIBUTE_PREFIX = "s0t:" + private const val KEY_IS_TEMPLATE = "is-template" + private const val KEY_OUTPUT_FILENAME = "output.filename" + private const val KEY_OUTPUT_DEFAULT_EXTENSION = "output.default-extension" + private const val DEF_OUTPUT_DEFAULT_EXTENSION = "puml" + private const val KEY_ACCEPTED_OUTPUT_EXTENSIONS = "output.accepted-extensions" + private const val DEF_ACCEPTED_OUTPUT_EXTENSIONS = "puml,iuml,md,adoc" + private const val STANDARD_TEMPLATES_DIRECTORY = "com/comsysto/livingdoc/s0t" + + override fun render(model: S0tModel) { + val env = environment().processingEnvironment + val templateDirectory = environment().resolveConfiguredPath(KEY_TPL_DIR)?.toFile() + + if (templateDirectory != null) { + val outputDirectory = environment().outputPath()!!.toFile() + val templateExtension = env.options.getOrDefault(KEY_TPL_EXT, DEF_TPL_EXT) + val freemarker = Configuration(VERSION_2_3_23) + + freemarker.templateLoader = MultiTemplateLoader(arrayOf( + FileTemplateLoader(templateDirectory), + ClassTemplateLoader(FreemarkerRenderer::class.java.classLoader, STANDARD_TEMPLATES_DIRECTORY))) + freemarker.addAutoImport("S0tTypes", "s0t-types.ftl") + freemarker.addAutoImport("S0tMembers", "s0t-members.ftl") + freemarker.addAutoImport("S0tRelations", "s0t-relations.ftl") + freemarker.addAutoImport("S0tSequences", "s0t-sequences.ftl") + + templateDirectory.walkTopDown() + .filter { it.isFile && it.name.endsWith(templateExtension) } + .forEach { loadAndRenderTemplate(it, freemarker, templateDirectory, outputDirectory, templateExtension, model) } + } + else { + log.debug("Skipping s0t freemarker generation: no template directory defined through {}", KEY_TPL_DIR) + } + } + + private fun loadAndRenderTemplate( + templateFile: File, + freemarker: Configuration, + templateDirectory: File, + outputDirectory: File, + templateExtension: String, + model: S0tModel + ) + { + val template = freemarker.getTemplate(templateFile.toRelativeString(templateDirectory)) + val cfg = s0tConfiguration(template) + + if (cfg.getBoolean(KEY_IS_TEMPLATE, false)) { + val outputFile = File(outputDirectory, outputFileName(cfg, templateFile, templateExtension, templateDirectory)) + .apply { parentFile.mkdirs() } + + log.info("Rendering template {} to {}", template.name, outputFile) + template.process(PlantUmlModel(templateDirectory.absolutePath, outputDirectory.absolutePath, model), outputFile.bufferedWriter(UTF_8)) + } + } + + /** + * Get the s0t-specific template configuration from the template's custom + * properties. + */ + private fun s0tConfiguration(template: Template): MapConfiguration = MapConfiguration(template.customAttributeNames + .filter { it.startsWith(CUSTOM_ATTRIBUTE_PREFIX) } + .groupBy({ it.substringAfter(CUSTOM_ATTRIBUTE_PREFIX) }, { template.getCustomAttribute(it) })) + + private fun outputFileName(cfg: MapConfiguration, it: File, templateExtension: String, outputDirectory: File): String { + return cfg.getString(KEY_OUTPUT_FILENAME, defaultOutputFile(cfg, it, templateExtension, outputDirectory).path) + } + + private fun defaultOutputFile(cfg: MapConfiguration, it: File, templateExtension: String, templateDirectory: File): File { + val filename = it.name.substringBeforeLast(templateExtension) + + return File(it.parentFile, if (hasAcceptedExtension(filename, cfg)) filename else filename + defaultExtension(cfg)) + .relativeTo(templateDirectory) + } + + private fun hasAcceptedExtension(filename: String, cfg: MapConfiguration) = acceptedExtensions(cfg).any { filename.endsWith(".${it}") } + private fun acceptedExtensions(cfg: MapConfiguration) = cfg.getString(KEY_ACCEPTED_OUTPUT_EXTENSIONS, DEF_ACCEPTED_OUTPUT_EXTENSIONS).split(",") + private fun defaultExtension(cfg: MapConfiguration) = cfg.getString(KEY_OUTPUT_DEFAULT_EXTENSION, DEF_OUTPUT_DEFAULT_EXTENSION) +} diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/render/OutputRenderer.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/render/OutputRenderer.kt new file mode 100644 index 0000000..6b26037 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/render/OutputRenderer.kt @@ -0,0 +1,8 @@ +package com.comsysto.livingdoc.s0t.render + +import com.comsysto.livingdoc.s0t.model.S0tModel + +interface OutputRenderer { + + fun render(model: S0tModel) +} \ No newline at end of file diff --git a/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/render/PlantUmlModel.kt b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/render/PlantUmlModel.kt new file mode 100644 index 0000000..f20d367 --- /dev/null +++ b/processors/src/main/kotlin/com/comsysto/livingdoc/s0t/render/PlantUmlModel.kt @@ -0,0 +1,9 @@ +package com.comsysto.livingdoc.s0t.render + +import com.comsysto.livingdoc.s0t.model.S0tModel + +data class PlantUmlModel( + val templateDirectory: String, + val outputDirectory: String, + val s0tModel: S0tModel +) \ No newline at end of file diff --git a/processors/src/main/resources/META-INF/services/javax.annotation.processing.Processor b/processors/src/main/resources/META-INF/services/javax.annotation.processing.Processor new file mode 100644 index 0000000..ff19340 --- /dev/null +++ b/processors/src/main/resources/META-INF/services/javax.annotation.processing.Processor @@ -0,0 +1 @@ +com.comsysto.livingdoc.s0t.S0tProcessor \ No newline at end of file diff --git a/processors/src/main/resources/com/comsysto/livingdoc/s0t/s0t-members.ftl b/processors/src/main/resources/com/comsysto/livingdoc/s0t/s0t-members.ftl new file mode 100644 index 0000000..7b77bc3 --- /dev/null +++ b/processors/src/main/resources/com/comsysto/livingdoc/s0t/s0t-members.ftl @@ -0,0 +1,21 @@ +<#macro renderField field> +<#-- @ftlvariable name="association" type="com.comsysto.livingdoc.s0t.model.FieldModel" --> + <@renderModifier field.accessModifier/>${field.type.name.simpleName}<@renderTypeParameters field.typeParameters/> ${field.name} + + +<#macro renderModifier modifier><#rt> + <#if modifier.name()="PUBLIC">+ <#lt><#rt> + <#elseif modifier.name()="PROTECTED"># <#lt><#rt> + <#elseif modifier.name()="PACKAGE">~ <#lt><#rt> + <#elseif modifier.name()="PRIVATE">- <#lt><#rt> + <#else><#lt><#rt> + <#lt><#rt> + + +<#macro renderTypeParameters params><#rt> +<#-- @ftlvariable name="params" type="java.util.List" --><#rt> + <#if params?size gt 0><#lt><#rt> + <<#list params as p>${p.name.simpleName}<#sep>, ><#lt><#rt> + <#lt><#rt> + + diff --git a/processors/src/main/resources/com/comsysto/livingdoc/s0t/s0t-relations.ftl b/processors/src/main/resources/com/comsysto/livingdoc/s0t/s0t-relations.ftl new file mode 100644 index 0000000..e11deac --- /dev/null +++ b/processors/src/main/resources/com/comsysto/livingdoc/s0t/s0t-relations.ftl @@ -0,0 +1,28 @@ +<#macro renderRelations s0tModel> +<#-- @ftlvariable name="s0tModel" type="com.comsysto.livingdoc.s0t.model.S0tModel" --> + <#list s0tModel.types?values as type> + + <#compress> + <#list type.realizations as realization> + ${realization.left.name.simpleName} <|.. ${realization.right.name.simpleName} + + <#if type.inheritance??> + ${type.inheritance.left.name.simpleName} <|-- ${type.inheritance.right.name.simpleName} + + <#list type.associations as association> + ${association.left.name.simpleName}<@renderCardinality association.sourceCardinality/><@renderArrow association/><@renderCardinality association.targetCardinality/>${association.right.name.simpleName} + + + + + + +<#macro renderCardinality c><#if c?length != 0> "${c}" <#else> + +<#macro renderArrow association><#rt> +<#-- @ftlvariable name="association" type="com.comsysto.livingdoc.s0t.model.Relation.Association" --><#lt><#rt> + <#if association.type.name() == "STANDARD"><#lt><#rt>--><#lt><#rt> + <#elseif association.type.name() == "COMPOSITION">*--><#lt><#rt> + <#elseif association.type.name() == "AGGREGATION">o--><#lt><#rt> + <#lt><#rt> +<#lt> diff --git a/processors/src/main/resources/com/comsysto/livingdoc/s0t/s0t-sequences.ftl b/processors/src/main/resources/com/comsysto/livingdoc/s0t/s0t-sequences.ftl new file mode 100644 index 0000000..aba4d0a --- /dev/null +++ b/processors/src/main/resources/com/comsysto/livingdoc/s0t/s0t-sequences.ftl @@ -0,0 +1,24 @@ +<#macro renderSequences s0tModel> +<#-- @ftlvariable name="s0tModel" type="com.comsysto.livingdoc.s0t.model.S0tModel" --> + <#list s0tModel.executables?keys as executableName> + participant ${executableName.typeName.simpleName} + + + <#list s0tModel.executables?values as source> + activate ${source.name.typeName.simpleName} + <@renderSequence source/> + deactivate ${source.name.typeName.simpleName} + + + +<#macro renderSequence source> +<#-- @ftlvariable name="source" type="com.comsysto.livingdoc.s0t.model.ExecutableModel" --> + <#list source.outgoingCalls as target> + ${source.name.typeName.simpleName} -> ${target.name.typeName.simpleName}<#if target.signature??>: ${target.signature} + <#if target.outgoingCalls?has_content> + activate ${target.name.typeName.simpleName} + <@renderSequence target/> + deactivate ${target.name.typeName.simpleName} + + + diff --git a/processors/src/main/resources/com/comsysto/livingdoc/s0t/s0t-types.ftl b/processors/src/main/resources/com/comsysto/livingdoc/s0t/s0t-types.ftl new file mode 100644 index 0000000..5e787fd --- /dev/null +++ b/processors/src/main/resources/com/comsysto/livingdoc/s0t/s0t-types.ftl @@ -0,0 +1,39 @@ +<#import "s0t-members.ftl" as Members> + +<#macro renderTypes s0tModel> +<#-- @ftlvariable name="s0tModel" type="com.comsysto.livingdoc.s0t.model.S0tModel" --> + <#list s0tModel.types?values as type> + <@renderType type/> + + + +<#macro renderType type> +<#-- @ftlvariable name="type" type="com.comsysto.livingdoc.s0t.model.TypeModel" --> + <#if type.type == "INTERFACE"> + <#local typeDeclaration="interface"> + <#elseif type.type == "ABSTRACT"> + <#local typeDeclaration="abstract class"> + <#elseif type.type == "ENUM"> + <#local typeDeclaration="enum"> + <#else> + <#local typeDeclaration="class"> + + ${typeDeclaration} ${type.name.simpleName}<#assign fields=type.fields><#if fields?has_content> { + <#list fields as field> + <#if type.type == "ENUM" && field.type.name = type.name> + ${field.name} + <#else> + <@Members.renderField field/> + + + } + + + <#list type.notes as note> + note ${note.position?lower_case} of ${type.name.simpleName} + ${note.text} + end note + + + + diff --git a/processors/src/main/resources/logback.xml b/processors/src/main/resources/logback.xml new file mode 100644 index 0000000..a211e06 --- /dev/null +++ b/processors/src/main/resources/logback.xml @@ -0,0 +1,13 @@ + + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file diff --git a/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Airplane.java b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Airplane.java new file mode 100644 index 0000000..1d5f945 --- /dev/null +++ b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Airplane.java @@ -0,0 +1,49 @@ +package com.comsysto.livingdoc.s0t.example; + + +import static com.comsysto.livingdoc.s0t.annotation.plantuml.Position.BOTTOM; +import static com.comsysto.livingdoc.s0t.annotation.plantuml.Position.RIGHT; + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass; +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlExecutable; +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlField; +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlField.AssociationType; +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlNote; +import com.comsysto.livingdoc.s0t.model.Relation.Association; + +@PlantUmlClass +@PlantUmlNote( + body = "This models an airplane, a //flying// \nvehicle that is **very** fast.", + position = BOTTOM) +public class Airplane extends FlyingVehicle { + + @PlantUmlField(associationType = AssociationType.COMPOSITION, targetCardinality = "2") + protected Tuple wings; + + @Override + @PlantUmlExecutable + @PlantUmlNote(position = RIGHT, body = "Directly after launch, the\nplane needs to **retract** its wheels.") + public void launch() { + super.launch(); + retractWheels(); + } + + @PlantUmlExecutable + private void retractWheels() { + } + + @Override + @PlantUmlExecutable + public void land() { + lowerWheels(); + super.land(); + } + + @PlantUmlExecutable + private void lowerWheels() { + } + + @PlantUmlExecutable + public void load() { + } +} diff --git a/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Airport.java b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Airport.java new file mode 100644 index 0000000..1d551b2 --- /dev/null +++ b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Airport.java @@ -0,0 +1,12 @@ +package com.comsysto.livingdoc.s0t.example; + + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlExecutable; + +public class Airport { + + @PlantUmlExecutable + public void load(final Airplane airplane) { + airplane.load(); + } +} diff --git a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Car.java b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Car.java similarity index 59% rename from annotation-processors/src/test/java/com/comsysto/livingdoc/example/Car.java rename to processors/src/test/java/com/comsysto/livingdoc/s0t/example/Car.java index b77f137..9912224 100644 --- a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/Car.java +++ b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Car.java @@ -1,6 +1,7 @@ -package com.comsysto.livingdoc.example; +package com.comsysto.livingdoc.s0t.example; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass; @PlantUmlClass(diagramIds = { "package", "ground-vehicles" }) public class Car extends GroundVehicle { diff --git a/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Flight.java b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Flight.java new file mode 100644 index 0000000..4c20f95 --- /dev/null +++ b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Flight.java @@ -0,0 +1,21 @@ +package com.comsysto.livingdoc.s0t.example; + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlExecutable; +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlExecutable.StartOfSequence; + +public class Flight { + + private Airport airport; + + private Airplane airplane; + + @PlantUmlExecutable + @StartOfSequence + public void execute() { + airport.load(airplane); + + airplane.launch(); + airplane.fly(); + airplane.land(); + } +} diff --git a/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Flying.java b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Flying.java new file mode 100644 index 0000000..901016d --- /dev/null +++ b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Flying.java @@ -0,0 +1,14 @@ +package com.comsysto.livingdoc.s0t.example; + + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass; + +@PlantUmlClass +public interface Flying { + + void launch(); + + void fly(); + + void land(); +} diff --git a/processors/src/test/java/com/comsysto/livingdoc/s0t/example/FlyingVehicle.java b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/FlyingVehicle.java new file mode 100644 index 0000000..6b23df5 --- /dev/null +++ b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/FlyingVehicle.java @@ -0,0 +1,21 @@ +package com.comsysto.livingdoc.s0t.example; + + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass; + +@PlantUmlClass +public class FlyingVehicle extends Vehicle implements Flying { + + @Override + public void launch() { + } + + @Override + public void fly() { + } + + @Override + public void land() { + + } +} diff --git a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/GroundVehicle.java b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/GroundVehicle.java similarity index 63% rename from annotation-processors/src/test/java/com/comsysto/livingdoc/example/GroundVehicle.java rename to processors/src/test/java/com/comsysto/livingdoc/s0t/example/GroundVehicle.java index 2e65102..93333e3 100644 --- a/annotation-processors/src/test/java/com/comsysto/livingdoc/example/GroundVehicle.java +++ b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/GroundVehicle.java @@ -1,9 +1,10 @@ -package com.comsysto.livingdoc.example; +package com.comsysto.livingdoc.s0t.example; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlClass; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlField; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlNote; -import com.comsysto.livingdoc.annotation.plantuml.PlantUmlNote.Position; + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass; +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlField; +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlNote; +import com.comsysto.livingdoc.s0t.annotation.plantuml.Position; @PlantUmlClass(diagramIds = { "package", "ground-vehicles" }) @PlantUmlNote(body = "A vehicle that drives on the ground", position = Position.TOP) diff --git a/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Train.java b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Train.java new file mode 100644 index 0000000..f507987 --- /dev/null +++ b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Train.java @@ -0,0 +1,19 @@ +package com.comsysto.livingdoc.s0t.example; + + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass; +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlField; + +import java.util.List; + +@PlantUmlClass(diagramIds = { "package", "ground-vehicles" }) +public class Train extends GroundVehicle { + + @PlantUmlField(targetCardinality = "0..*") + public List loadedCars; + + public Train(final List loadedCars, int numberOfWheels) { + this.loadedCars = loadedCars; + this.setNumberOfWheels(numberOfWheels); + } +} diff --git a/processors/src/test/java/com/comsysto/livingdoc/s0t/example/TransportType.java b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/TransportType.java new file mode 100644 index 0000000..6677581 --- /dev/null +++ b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/TransportType.java @@ -0,0 +1,17 @@ +package com.comsysto.livingdoc.s0t.example; + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass; +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlNote; + +@PlantUmlClass +@PlantUmlNote(body = "Indicates whether a vehicle transports passengers or cargo.") +public enum TransportType { + PASSENGERS("Persons are passengers."), + CARGO("Bags are cargo."); + + final String description; + + TransportType(final String description) { + this.description = description; + } +} diff --git a/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Tuple.java b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Tuple.java new file mode 100644 index 0000000..a7c87c0 --- /dev/null +++ b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Tuple.java @@ -0,0 +1,13 @@ +package com.comsysto.livingdoc.s0t.example; + +/** + * A tuple type, serving as example for a custom container type that won't + * show up in diagrams as an association target. + * + * @param + * @param + */ +public class Tuple { + T1 left; + T2 right; +} diff --git a/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Vehicle.java b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Vehicle.java new file mode 100644 index 0000000..cdb6119 --- /dev/null +++ b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Vehicle.java @@ -0,0 +1,12 @@ +package com.comsysto.livingdoc.s0t.example; + + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass; +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlField; + +@PlantUmlClass(diagramIds = { "package", "ground-vehicles" }) +public class Vehicle { + + @PlantUmlField + public TransportType transportType; +} diff --git a/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Wing.java b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Wing.java new file mode 100644 index 0000000..d69d182 --- /dev/null +++ b/processors/src/test/java/com/comsysto/livingdoc/s0t/example/Wing.java @@ -0,0 +1,8 @@ +package com.comsysto.livingdoc.s0t.example; + + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlClass; + +@PlantUmlClass +public class Wing { +} diff --git a/processors/src/test/java/freemarker_implicit.ftl b/processors/src/test/java/freemarker_implicit.ftl new file mode 100644 index 0000000..0494959 --- /dev/null +++ b/processors/src/test/java/freemarker_implicit.ftl @@ -0,0 +1,6 @@ +<#ftl> +<#-- @implicitly included --> +<#import "com/comsysto/livingdoc/s0t/s0t-types.ftl" as S0tTypes> +<#import "com/comsysto/livingdoc/s0t/s0t-members.ftl" as S0tMembers> +<#import "com/comsysto/livingdoc/s0t/s0t-relations.ftl" as S0tRelations> +<#import "com/comsysto/livingdoc/s0t/s0t-sequences.ftl" as S0tSequences> diff --git a/processors/src/test/java/s0t.yaml b/processors/src/test/java/s0t.yaml new file mode 100644 index 0000000..9a36697 --- /dev/null +++ b/processors/src/test/java/s0t.yaml @@ -0,0 +1,4 @@ +s0t: + freemarker: + template: + root: ../resources \ No newline at end of file diff --git a/processors/src/test/kotlin/com/comsysto/livingdoc/s0t/S0TProcessorTest.kt b/processors/src/test/kotlin/com/comsysto/livingdoc/s0t/S0TProcessorTest.kt new file mode 100644 index 0000000..1cd9ffb --- /dev/null +++ b/processors/src/test/kotlin/com/comsysto/livingdoc/s0t/S0TProcessorTest.kt @@ -0,0 +1,64 @@ +package com.comsysto.livingdoc.s0t + +import com.google.testing.compile.Compilation +import com.google.testing.compile.Compiler.javac +import com.google.testing.compile.JavaFileObjects +import io.kotlintest.matchers.collections.shouldContainExactly +import io.kotlintest.matchers.file.exist +import io.kotlintest.should +import io.kotlintest.shouldBe +import io.kotlintest.specs.BehaviorSpec +import java.io.File +import java.nio.file.Files +import java.nio.file.Paths +import java.util.stream.Collectors + +internal class S0TProcessorTest : BehaviorSpec({ + Given("the test directory") { + val testDir = System.getProperty(KEY_TEST_DIR) + val packagePath = "com/comsysto/livingdoc/s0t/example" + val exampleDir = File(if (testDir != null) File(testDir) else testSourcesRoot(), packagePath) + val sut = S0tProcessor() + + When("I run the annotation processor") { + val files = exampleDir.listFiles() + if (files == null || files.isEmpty()) throw IllegalStateException("$exampleDir contains no source files.") + + val options = System.getProperties() + .filter { with(it.key.toString()) { startsWith("s0t.") && !startsWith("s0t.test.") } } + .map { "-A${it.key}=${it.value}" } + + val result = javac() + .withProcessors(sut) + .withOptions(options) + .compile(files.map { javaFileObject(it) }) + val classDiagramFile = File(System.getProperty(KEY_OUT_DIR, DEF_OUT_DIR), "${packagePath}/example-class.puml") + val sequenceDiagramFile = File(System.getProperty(KEY_OUT_DIR, DEF_OUT_DIR), "${packagePath}/example-sequence.puml") + + Then("the processing status should be SUCCESS") { result.status() shouldBe Compilation.Status.SUCCESS } + + And("the class diagram file exists") { classDiagramFile should exist() } + And("the class diagram contains the expected content") { + nonEmptyLinesTrimmed(classDiagramFile) shouldContainExactly nonEmptyLinesTrimmed(File(testClassLoaderRoot(), "/expected/example-class.puml")) + } + And("the sequence diagram file exists") { sequenceDiagramFile should exist() } + And("the sequence diagram contains the expected content") { + nonEmptyLinesTrimmed(sequenceDiagramFile) shouldContainExactly nonEmptyLinesTrimmed(File(testClassLoaderRoot(), "/expected/example-sequence.puml")) + } + } + } +}) { + companion object { + private const val KEY_TEST_DIR = "s0t.test.dir" + + private fun testSourcesRoot() = File(testClassLoaderRoot().parentFile.parentFile, "src/test/java") + private fun testClassLoaderRoot() = Paths.get(this::class.java.classLoader.getResource(".")!!.toURI()).toFile() + + private fun javaFileObject(f: File) = JavaFileObjects.forResource(f.toURI().toURL()) + + private fun nonEmptyLinesTrimmed(file: File) = Files.lines(file.toPath()) + .map { it -> it.trim { it == ' ' } } + .filter { it.isNotEmpty()} + .collect(Collectors.toList()) + } +} diff --git a/processors/src/test/kotlin/com/comsysto/livingdoc/s0t/TestUtils.kt b/processors/src/test/kotlin/com/comsysto/livingdoc/s0t/TestUtils.kt new file mode 100644 index 0000000..c660b94 --- /dev/null +++ b/processors/src/test/kotlin/com/comsysto/livingdoc/s0t/TestUtils.kt @@ -0,0 +1,20 @@ +package com.comsysto.livingdoc.s0t + +import com.comsysto.livingdoc.s0t.model.TypeName +import com.comsysto.livingdoc.s0t.model.TypeRef +import io.mockk.every +import io.mockk.mockk +import javax.lang.model.element.Name +import kotlin.reflect.KClass + +object TestUtils { + fun typeRef(type: KClass<*>) = typeRef(typeName(type)) + fun typeRef(typeName: TypeName) = TypeRef(typeName, TypeRef.Kind.COMPLEX) + fun typeName(type: KClass<*>) = TypeName.parse(type.java.name) + + fun name(s: String): Name { + val name = mockk() + every { name.toString() } returns s + return name + } +} \ No newline at end of file diff --git a/processors/src/test/kotlin/com/comsysto/livingdoc/s0t/model/RelationTest.kt b/processors/src/test/kotlin/com/comsysto/livingdoc/s0t/model/RelationTest.kt new file mode 100644 index 0000000..b7caabf --- /dev/null +++ b/processors/src/test/kotlin/com/comsysto/livingdoc/s0t/model/RelationTest.kt @@ -0,0 +1,100 @@ +package com.comsysto.livingdoc.s0t.model + +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlField +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlField.AssociationType +import com.comsysto.livingdoc.s0t.model.Relation.* +import com.comsysto.livingdoc.s0t.model.S0tModelTestObjectMother.annotatedClassTypeElement +import com.comsysto.livingdoc.s0t.model.S0tModelTestObjectMother.fieldTypeElement +import com.comsysto.livingdoc.s0t.model.S0tModelTestObjectMother.interfaceTypeElement +import com.comsysto.livingdoc.s0t.model.S0tModelTestObjectMother.plantUmlFieldAnnotation +import com.comsysto.livingdoc.s0t.model.S0tModelTestObjectMother.superClassTypeElement +import com.comsysto.livingdoc.s0t.model.S0tModelTestObjectMother.typeElement +import com.comsysto.livingdoc.s0t.model.S0tModelTestObjectMother.variableElement +import io.kotlintest.shouldBe +import io.kotlintest.specs.BehaviorSpec +import io.mockk.every +import io.mockk.mockk +import javax.lang.model.element.ElementKind +import javax.lang.model.type.DeclaredType + +internal class RelationTest : BehaviorSpec({ + Given("A type element annotated with @PlantUmlClass") { + + When("I create an inheritance from a type element'") { + val result = Inheritance.of(annotatedClassTypeElement) + + Then ("it should return the inheritance relation") { + result shouldBe Inheritance(TypeRef.of(superClassTypeElement), TypeRef.of(annotatedClassTypeElement)) + } + } + + When("I create realizations from a type element") { + val result = Realization.allOf(annotatedClassTypeElement) + + Then("it should return a relation to the interface annotated with @PlantUmlClass") { + result shouldBe listOf(Realization(TypeRef.of(interfaceTypeElement), TypeRef.of(annotatedClassTypeElement))) + } + } + + When("I create field associations from a type element") { + val result = Association.allOf(annotatedClassTypeElement) + + Then("it should return the association annotated with @PlantUmlField") { + val field = FieldModel("field1", TypeRef.of(fieldTypeElement), AccessModifier.PACKAGE) + result shouldBe listOf(Association(TypeRef.of(annotatedClassTypeElement), field.type, field)) + } + } + + When("I create field associations from a variable element with several properties") { + val annotation = mockk().apply { + every { associationType } returns AssociationType.AGGREGATION + every { sourceCardinality } returns "1" + every { targetCardinality } returns "0..5" + every { forceStandardTypeAssociation } returns false + } + + val result = Association.allOf(variableElement("containerField", fieldTypeElement, annotation), annotatedClassTypeElement) + + Then("it should return a single association with the specified properties") { + val field = FieldModel("containerField", TypeRef.of(fieldTypeElement), AccessModifier.PACKAGE) + result shouldBe listOf(Association(TypeRef.of(annotatedClassTypeElement), field.type, field, "1", "0..5", AssociationType.AGGREGATION)) + } + } + + When("I get the target types for a declared type") { + val containerType = typeElement( + "my.package.MyContainerType", + ElementKind.CLASS, + listOf(typeElement("1"), typeElement("2"), typeElement("1")).map { it.asType() as DeclaredType } + ) + + val result = Association.targetTypes(containerType.asType() as DeclaredType) + + Then("it should return a set containing every distinct type") { + result shouldBe setOf(TypeRef.of(typeElement("1", elementKind = ElementKind.CLASS)), TypeRef.of(typeElement( + "2", + elementKind = ElementKind.CLASS + ))) + } + } + + When("I create field associations from a variable element with a container target type") { + val typeArgs = listOf(typeElement("1"), typeElement("2")).map { it.asType() as DeclaredType } + val t1 = TypeRef.of(typeArgs[0]) + val t2 = TypeRef.of(typeArgs[1]) + val fieldName = "field" + val containerTypeElement = typeElement("containerField", ElementKind.CLASS, typeArgs) + val field = variableElement(fieldName, containerTypeElement, plantUmlFieldAnnotation()) + val result = Association.allOf(field, annotatedClassTypeElement) + + Then("it should return a list of associations to each parameter types, but not to the container type itself") { + val f = FieldModel(fieldName, TypeRef.of(containerTypeElement), AccessModifier.PACKAGE, listOf(t1, t2)) + + result shouldBe listOf( + Association(TypeRef.of(annotatedClassTypeElement), t1, f), + Association(TypeRef.of(annotatedClassTypeElement), t2, f) + ) + } + } + } +}) diff --git a/processors/src/test/kotlin/com/comsysto/livingdoc/s0t/model/S0tModelTestObjectMother.kt b/processors/src/test/kotlin/com/comsysto/livingdoc/s0t/model/S0tModelTestObjectMother.kt new file mode 100644 index 0000000..1bca924 --- /dev/null +++ b/processors/src/test/kotlin/com/comsysto/livingdoc/s0t/model/S0tModelTestObjectMother.kt @@ -0,0 +1,132 @@ +package com.comsysto.livingdoc.s0t.model + +import com.comsysto.livingdoc.s0t.TestUtils.name +import com.comsysto.livingdoc.s0t.annotation.plantuml.* +import com.comsysto.livingdoc.s0t.annotation.plantuml.PlantUmlField.AssociationType +import com.comsysto.livingdoc.s0t.asTypeElement +import io.mockk.every +import io.mockk.mockk +import javax.lang.model.element.* +import javax.lang.model.type.DeclaredType +import javax.lang.model.type.TypeKind + +object S0tModelTestObjectMother { + val superClassTypeName = name("my.package.name.MySuperClass") + + val plantUmlClassAnnotation = mockk().apply { + every { autoCreateFields } returns AutoCreateType.NO + every { autoCreateAssociations } returns AutoCreateType.NO + } + + val superClassTypeElement = mockk().apply { + every { qualifiedName } returns superClassTypeName + every { getAnnotation(PlantUmlClass::class.java) } returns plantUmlClassAnnotation + every { kind } returns ElementKind.CLASS + } + + val superClassType = mockk().apply { + every { asTypeElement() } returns superClassTypeElement + } + + const val interfaceTypeName = "my.package.MyInterface" + val interfaceTypeElement = mockInterfaceTypeElement(interfaceTypeName, mockk()) + val notAnnotatedInterfaceType = + mockk().apply { every { asTypeElement() } returns mockInterfaceTypeElement("some.other.InterfaceNotAnnotated") } + val annotatedInterfaceType = mockk().apply { every { asTypeElement() } returns interfaceTypeElement } + + fun plantUmlFieldAnnotation( + pShowAssociation: Boolean = true, + pForceStandardTypeAssociation: Boolean = false, + pSourceCardinality: String = "", + pTargetCardinality: String = "", + pAssociationType: AssociationType = AssociationType.STANDARD + ) = mockk().apply { + every { showAssociation } returns pShowAssociation + every { forceStandardTypeAssociation } returns pForceStandardTypeAssociation + every { sourceCardinality } returns pSourceCardinality + every { targetCardinality } returns pTargetCardinality + every { associationType } returns pAssociationType + } + + val annotatedClassTypeElement = typeElement("my.package.name.MyType").apply { + every { superclass } returns superClassType + every { interfaces } returns listOf(notAnnotatedInterfaceType, annotatedInterfaceType) + every { enclosedElements } returns listOf( + variableElement("field1", typeElement("my.package.MyFieldType"), plantUmlFieldAnnotation()), + variableElement("field2", typeElement("my.package.MyFieldType")), + constructorElement, + methodElement + ) + every { getAnnotation(PlantUmlClass::class.java) } returns plantUmlClassAnnotation + every { getAnnotation(PlantUmlNotes::class.java) } returns mockPlantUmlNotes(note1, note2) + } + + val constructorElement = mockk() + val methodElement = mockk() + val fieldTypeElement = typeElement("my.package.MyFieldType") + + /** + * Creates a complex type element together with its type mirror. + * + * @param name the name of the type. + * @param typeArgs a list of type arguments. + * + * @return the type element. + */ + fun typeElement( + name: String, + elementKind: ElementKind = ElementKind.CLASS, + typeArgs: List = emptyList(), + annotation: PlantUmlClass? = plantUmlClassAnnotation + ): TypeElement { + val typeElement = mockk() + + val typeMirror: DeclaredType = mockk().apply { + every { kind } returns TypeKind.DECLARED + every { typeArguments } returns typeArgs + } + + every { typeMirror.asTypeElement() } returns typeElement + typeElement.apply { + every { qualifiedName } returns name(name) + every { asType() } returns typeMirror + every { kind } returns elementKind + every { getAnnotation(PlantUmlClass::class.java) } returns annotation + } + return typeElement + } + + val fieldElement = variableElement("field", typeElement("my.package.MyFieldType"), null) + + fun variableElement(name: String, typeElement: TypeElement, annotation: PlantUmlField? = null) = mockk().apply { + every { simpleName } returns name(name) + every { asType() } returns typeElement.asType() + annotation.apply { every { getAnnotation(PlantUmlField::class.java) } returns annotation } + every { modifiers } returns setOf(Modifier.DEFAULT) + } + + val annotatedFieldElement = variableElement("myField", fieldTypeElement, plantUmlFieldAnnotation()) + + val note1 = mockPlantUmlNote("A note", Position.RIGHT) + val note2 = mockPlantUmlNote("Another note", Position.TOP) + + fun mockPlantUmlNotes(vararg plantUmlNotes: PlantUmlNote): PlantUmlNotes { + val notesAnnotation = mockk() + every { notesAnnotation.value } returns plantUmlNotes.map { it }.toTypedArray() + return notesAnnotation + } + + fun mockPlantUmlNote(body: String, position: Position): PlantUmlNote { + val plantUmlNote = mockk() + every { plantUmlNote.body } returns body + every { plantUmlNote.position } returns com.comsysto.livingdoc.s0t.annotation.plantuml.Position.valueOf(position.name) + return plantUmlNote + } + + fun mockInterfaceTypeElement(interfaceTypeName: String, plantUmlClassAnnotation: PlantUmlClass? = null) = mockk { + every { qualifiedName } returns name(interfaceTypeName) + every { getAnnotation(PlantUmlClass::class.java) } returns plantUmlClassAnnotation + every { kind } returns ElementKind.INTERFACE + } + +} \ No newline at end of file diff --git a/processors/src/test/resources/com/comsysto/livingdoc/s0t/example/example-class.puml.s0t.ftl b/processors/src/test/resources/com/comsysto/livingdoc/s0t/example/example-class.puml.s0t.ftl new file mode 100644 index 0000000..9c246d1 --- /dev/null +++ b/processors/src/test/resources/com/comsysto/livingdoc/s0t/example/example-class.puml.s0t.ftl @@ -0,0 +1,12 @@ +<#ftl attributes={"s0t:is-template":true}/> +<#-- @ftlvariable name="s0tModel" type="com.comsysto.livingdoc.s0t.model.S0tModel" --> +@startuml +!include ./format.iuml +hide empty members +title Example class diagram + +<@S0tTypes.renderTypes s0tModel/> + +<@S0tRelations.renderRelations s0tModel/> + +@enduml diff --git a/processors/src/test/resources/com/comsysto/livingdoc/s0t/example/example-sequence.puml.s0t.ftl b/processors/src/test/resources/com/comsysto/livingdoc/s0t/example/example-sequence.puml.s0t.ftl new file mode 100644 index 0000000..26102b2 --- /dev/null +++ b/processors/src/test/resources/com/comsysto/livingdoc/s0t/example/example-sequence.puml.s0t.ftl @@ -0,0 +1,9 @@ +<#ftl attributes={"s0t:is-template":true}/> +<#-- @ftlvariable name="s0tModel" type="com.comsysto.livingdoc.s0t.model.S0tModel" --> +@startuml +!include ./format.iuml +title Example sequence diagram + +<@S0tSequences.renderSequences s0tModel/> + +@enduml diff --git a/processors/src/test/resources/com/comsysto/livingdoc/s0t/example/format.iuml.s0t.ftl b/processors/src/test/resources/com/comsysto/livingdoc/s0t/example/format.iuml.s0t.ftl new file mode 100644 index 0000000..63a4ddf --- /dev/null +++ b/processors/src/test/resources/com/comsysto/livingdoc/s0t/example/format.iuml.s0t.ftl @@ -0,0 +1,39 @@ +<#ftl attributes={"s0t:is-template":true}/> +skinparam class { + BackgroundColor #F0F0F0/#C0C0C0 + BorderColor grey + BackgroundColor<> #e6ffe6/#b3ffb3 + BorderColor<> green + BorderColor<> green + BorderColor<> blue + BackgroundColor<> #e6e6ff/#b3b3ff + BackgroundColor<> #ffd700/yellow + BorderColor<> orange + ArrowColor black +} + +skinParam noteBackgroundColor #FFFFAA +skinParam noteBorderColor gray + +skinparam sequence { + BoxBackgroundColor #FAFAFA + BoxBorderColor grey + ArrowColor black + ActorBorderColor grey + LifeLineBorderColor black + LifeLineBackgroundColor #E0E0E0|#FFFFFF + + ParticipantBorderColor grey + ParticipantBackgroundColor #F0F0F0/#C0C0C0 + ParticipantBorderColor<> green + ParticipantBackgroundColor<> #e6ffe6/#b3ffb3 + ParticipantBorderColor<> green + ParticipantBorderColor<> blue + ParticipantBackgroundColor<> #e6e6ff/#b3b3ff + ParticipantBackgroundColor<> #ffd700/yellow + ParticipantBorderColor<> orange + ParticipantFontColor black + + ActorBackgroundColor #F0F0F0/#C0C0C0 + ActorBackgroundColor<> #e6ffe6/#b3ffb3 +} diff --git a/processors/src/test/resources/expected/example-class.puml b/processors/src/test/resources/expected/example-class.puml new file mode 100644 index 0000000..0daa3cd --- /dev/null +++ b/processors/src/test/resources/expected/example-class.puml @@ -0,0 +1,57 @@ +@startuml +!include ./format.iuml +hide empty members +title Example class diagram + +class Airplane { + # Tuple wings +} + +class Car +interface Flying +class FlyingVehicle +abstract class GroundVehicle { + - int numberOfWheels +} + +note top of GroundVehicle +A vehicle that drives on the ground +end note + +note right of GroundVehicle +Multiple notes may be +attached to a type +end note + +class Train { + + List loadedCars +} + +enum TransportType { + PASSENGERS + CARGO + ~ String description +} + +class Vehicle { + + TransportType transportType +} + +class Wing + +FlyingVehicle <|-- Airplane +Airplane *--> "2" Wing + +GroundVehicle <|-- Car + +Flying <|.. FlyingVehicle +Vehicle <|-- FlyingVehicle + +Vehicle <|-- GroundVehicle + +GroundVehicle <|-- Train +Train --> "0..*" Car + +Vehicle --> TransportType + +@enduml diff --git a/processors/src/test/resources/expected/example-sequence.puml b/processors/src/test/resources/expected/example-sequence.puml new file mode 100644 index 0000000..76280c5 --- /dev/null +++ b/processors/src/test/resources/expected/example-sequence.puml @@ -0,0 +1,25 @@ +@startuml +!include ./format.iuml +title Example sequence diagram + + participant Flight + + activate Flight + Flight -> Airport: load(com.comsysto.livingdoc.s0t.example.Airplane) + activate Airport + Airport -> Airplane: load() + deactivate Airport + Flight -> Airplane: launch() + activate Airplane + Airplane -> FlyingVehicle: launch() + Airplane -> Airplane: retractWheels() + deactivate Airplane + Flight -> FlyingVehicle: fly() + Flight -> Airplane: land() + activate Airplane + Airplane -> Airplane: lowerWheels() + Airplane -> FlyingVehicle: land() + deactivate Airplane + deactivate Flight + +@enduml diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index a2475df..0000000 --- a/settings.gradle +++ /dev/null @@ -1,5 +0,0 @@ -rootProject.name = 'living-documentation-lab' - -include 'annotation-processors' -include 'annotations' -