From 5bd961d046d3c48dc07dc3c4eda922ee44ce29d9 Mon Sep 17 00:00:00 2001 From: azerr Date: Fri, 29 Nov 2019 19:51:15 +0100 Subject: [PATCH] Improve compute of Quarkus/Microprofile properties Fixes #154 Signed-off-by: azerr --- .../META-INF/MANIFEST.MF | 4 +- .../commons/MetadataProperty.java | 303 +++++++++++ .../commons/MicroProfileProjectInfo.java | 51 ++ .../commons/ReferenceProperties.java | 26 + .../jdt/core/AbstractPropertiesProvider.java | 77 +++ .../jdt/core/ArtifactResolver.java | 16 + .../jdt/core/IPropertiesCollector.java | 15 + .../jdt/core/IPropertiesProvider.java | 34 ++ .../jdt/core/PropertiesManager.java | 230 ++++++++ .../microprofile/jdt/core/SearchContext.java | 22 + .../jdt/core/utils/JDTTypeUtils.java | 109 ++++ .../jdt/internal/core/FakeJavaProject.java | 52 ++ .../internal/core/MavenArtifactResolver.java | 55 ++ .../MicroProfileConfigPropertyProvider.java | 68 +++ .../core/MicroProfileRestClientProvider.java | 61 +++ .../internal/core/PropertiesCollector.java | 77 +++ .../core/QuarkusConfigPropertiesProvider.java | 361 +++++++++++++ .../core/QuarkusConfigRootProvider.java | 507 ++++++++++++++++++ 18 files changed, 2067 insertions(+), 1 deletion(-) create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/commons/MetadataProperty.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/commons/MicroProfileProjectInfo.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/commons/ReferenceProperties.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/AbstractPropertiesProvider.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/ArtifactResolver.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/IPropertiesCollector.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/IPropertiesProvider.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/PropertiesManager.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/SearchContext.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/utils/JDTTypeUtils.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/FakeJavaProject.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/MavenArtifactResolver.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/MicroProfileConfigPropertyProvider.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/MicroProfileRestClientProvider.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/PropertiesCollector.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/QuarkusConfigPropertiesProvider.java create mode 100644 quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/QuarkusConfigRootProvider.java diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/META-INF/MANIFEST.MF b/quarkus.jdt/com.redhat.quarkus.jdt.core/META-INF/MANIFEST.MF index 84066991a..ce5ff4e13 100644 --- a/quarkus.jdt/com.redhat.quarkus.jdt.core/META-INF/MANIFEST.MF +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/META-INF/MANIFEST.MF @@ -14,5 +14,7 @@ Require-Bundle: org.eclipse.core.runtime, org.eclipse.lsp4j Bundle-RequiredExecutionEnvironment: JavaSE-1.8 Bundle-ActivationPolicy: lazy -Export-Package: com.redhat.quarkus.commons, +Export-Package: com.redhat.microprofile.commons, + com.redhat.microprofile.jdt.core, + com.redhat.quarkus.commons, com.redhat.quarkus.jdt.core diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/commons/MetadataProperty.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/commons/MetadataProperty.java new file mode 100644 index 000000000..25ba24917 --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/commons/MetadataProperty.java @@ -0,0 +1,303 @@ +package com.redhat.microprofile.commons; + +import java.util.List; + +import com.redhat.quarkus.commons.EnumItem; +import com.redhat.quarkus.commons.ExtendedConfigDescriptionBuildItem; + +public class MetadataProperty { + + /** + * Values are read and available for usage at build time. + */ + public static final int CONFIG_PHASE_BUILD_TIME = 1; + /** + * Values are read and available for usage at build time, and available on a + * read-only basis at run time. + */ + public static final int CONFIG_PHASE_BUILD_AND_RUN_TIME_FIXED = 2; + /** + * Values are read and available for usage at run time and are re-read on each + * program execution. + */ + public static final int CONFIG_PHASE_RUN_TIME = 3; + + private String propertyName; + private String type; + private String defaultValue; + private String docs; + + private String extensionName; + private String location; + private String source; + private boolean required; + private int phase; + private List enums; + + public String getPropertyName() { + return propertyName; + } + + public void setPropertyName(String propertyName) { + this.propertyName = propertyName; + } + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public String getDefaultValue() { + return defaultValue; + } + + public void setDefaultValue(String defaultValue) { + this.defaultValue = defaultValue; + } + + public String getDocs() { + return docs; + } + + public void setDocs(String docs) { + this.docs = docs; + } + + public String getSource() { + return source; + } + + public void setSource(String source) { + this.source = source; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + public String getExtensionName() { + return extensionName; + } + + public void setExtensionName(String extensionName) { + this.extensionName = extensionName; + } + + public boolean isRequired() { + return required; + } + + public void setRequired(boolean required) { + this.required = required; + } + + /** + * Set the enumeration list of the property + * + * @param enums the enumeration list of the property + */ + public void setEnums(List enums) { + this.enums = enums; + } + + /** + * Returns the enumeration list of the property and null otherwise. + * + * @return the enumeration list of the property and null otherwise. + */ + public List getEnums() { + return enums; + } + + /** + * Returns the enumeration item from the given property value and null + * otherwise. + * + * @param propertyValue the property value. + * @return the enumeration item from the given property value and null + * otherwise. + */ + public EnumItem getEnumItem(String propertyValue) { + return getEnumItem(propertyValue, enums); + } + + /** + * Returns true if the given property value is a valid enumeration and false + * otherwise. + * + * @param propertyValue the property value. + * @return true if the given property value is a valid enumeration and false + * otherwise. + */ + public boolean isValidEnum(String propertyValue) { + return isValidEnum(propertyValue, enums); + } + + /** + * Returns the enumeration item from the given property value and given + * enumerations and null otherwise. + * + * @param propertyValue the property value. + * @param enums enumerations. + * @return the enumeration item from the given property value and given + * enumerations and null otherwise. + */ + public static EnumItem getEnumItem(String propertyValue, List enums) { + if (enums == null || propertyValue == null || propertyValue.isEmpty()) { + return null; + } + for (EnumItem enumItem : enums) { + if (propertyValue.equals(enumItem.getName())) { + return enumItem; + } + } + return null; + } + + /** + * Returns true if the given property value is a valid enumeration and false + * otherwise. + * + * @param propertyValue the property value. + * @param enums enumerations. + * @return true if the given property value is a valid enumeration and false + * otherwise. + */ + public static boolean isValidEnum(String propertyValue, List enums) { + if (enums == null || enums.isEmpty()) { + return true; + } + return getEnumItem(propertyValue, enums) != null; + } + + public int getPhase() { + return phase; + } + + public void setPhase(int phase) { + this.phase = phase; + } + + public boolean isAvailableAtRun() { + return phase == CONFIG_PHASE_BUILD_AND_RUN_TIME_FIXED || phase == CONFIG_PHASE_RUN_TIME; + } + + public boolean isBooleanType() { + return "boolean".equals(getType()) || // + "java.lang.Boolean".equals(getType()) || // + "java.util.Optional".equals(getType()); + } + + public boolean isIntegerType() { + return "int".equals(getType()) || // + "java.lang.Integer".equals(getType()) || // + "java.util.OptionalInt".equals(getType()) || // + "java.util.Optional".equals(getType()); + } + + public boolean isFloatType() { + return "float".equals(getType()) || // + "java.lang.Float".equals(getType()) || // + "java.util.Optional".equals(getType()); + } + + public boolean isLongType() { + return "long".equals(getType()) || // + "java.lang.Long".equals(getType()) || // + "java.util.OptionalLong".equals(getType()) || // + "java.util.Optional".equals(getType()); + } + + public boolean isDoubleType() { + return "double".equals(getType()) || // + "java.lang.Double".equals(getType()) || // + "java.util.OptionalDouble".equals(getType()) || // + "java.util.Optional".equals(getType()); + } + + public boolean isShortType() { + return "short".equals(getType()) || // + "java.lang.Short".equals(getType()) || // + "java.util.Optional".equals(getType()); + } + + @Override + public int hashCode() { + final int prime = 31; + int result = 1; + result = prime * result + ((defaultValue == null) ? 0 : defaultValue.hashCode()); + result = prime * result + ((docs == null) ? 0 : docs.hashCode()); + result = prime * result + ((enums == null) ? 0 : enums.hashCode()); + result = prime * result + ((extensionName == null) ? 0 : extensionName.hashCode()); + result = prime * result + ((location == null) ? 0 : location.hashCode()); + result = prime * result + phase; + result = prime * result + ((propertyName == null) ? 0 : propertyName.hashCode()); + result = prime * result + (required ? 1 : 0); + result = prime * result + ((source == null) ? 0 : source.hashCode()); + result = prime * result + ((type == null) ? 0 : type.hashCode()); + return result; + } + + @Override + public boolean equals(Object obj) { + if (this == obj) + return true; + if (obj == null) + return false; + if (getClass() != obj.getClass()) + return false; + MetadataProperty other = (MetadataProperty) obj; + if (defaultValue == null) { + if (other.defaultValue != null) + return false; + } else if (!defaultValue.equals(other.defaultValue)) + return false; + if (docs == null) { + if (other.docs != null) + return false; + } else if (!docs.equals(other.docs)) + return false; + if (enums == null) { + if (other.enums != null) + return false; + } else if (!enums.equals(other.enums)) + return false; + if (extensionName == null) { + if (other.extensionName != null) + return false; + } else if (!extensionName.equals(other.extensionName)) + return false; + if (location == null) { + if (other.location != null) + return false; + } else if (!location.equals(other.location)) + return false; + if (phase != other.phase) + return false; + if (propertyName == null) { + if (other.propertyName != null) + return false; + } else if (!propertyName.equals(other.propertyName)) + return false; + if (required != other.required) + return false; + if (source == null) { + if (other.source != null) + return false; + } else if (!source.equals(other.source)) + return false; + if (type == null) { + if (other.type != null) + return false; + } else if (!type.equals(other.type)) + return false; + return true; + } +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/commons/MicroProfileProjectInfo.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/commons/MicroProfileProjectInfo.java new file mode 100644 index 000000000..17b2d65d6 --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/commons/MicroProfileProjectInfo.java @@ -0,0 +1,51 @@ +package com.redhat.microprofile.commons; + +import java.util.List; + +import com.redhat.quarkus.commons.ClasspathKind; + +public class MicroProfileProjectInfo { + + public static final String DEFAULT_REFERENCE_TYPE = "${property}"; + + private String projectURI; + + private ClasspathKind classpathKind; + + private List properties; + + /** + * Returns the project URI. + * + * @return the project URI. + */ + public String getProjectURI() { + return projectURI; + } + + /** + * Set the project URI. + * + * @param projectURI the project URI. + */ + public void setProjectURI(String projectURI) { + this.projectURI = projectURI; + } + + public ClasspathKind getClasspathKind() { + return classpathKind; + } + + public void setClasspathKind(ClasspathKind classpathKind) { + this.classpathKind = classpathKind; + } + + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } + +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/commons/ReferenceProperties.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/commons/ReferenceProperties.java new file mode 100644 index 000000000..23ec252db --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/commons/ReferenceProperties.java @@ -0,0 +1,26 @@ +package com.redhat.microprofile.commons; + +import java.util.List; + +public class ReferenceProperties { + + private String type; + private List properties; + + public String getType() { + return type; + } + + public void setType(String type) { + this.type = type; + } + + public List getProperties() { + return properties; + } + + public void setProperties(List properties) { + this.properties = properties; + } + +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/AbstractPropertiesProvider.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/AbstractPropertiesProvider.java new file mode 100644 index 000000000..4aac83ce2 --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/AbstractPropertiesProvider.java @@ -0,0 +1,77 @@ +package com.redhat.microprofile.jdt.core; + +import static com.redhat.quarkus.jdt.internal.core.utils.AnnotationUtils.isMatchAnnotation; + +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.IAnnotatable; +import org.eclipse.jdt.core.IAnnotation; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.search.IJavaSearchConstants; +import org.eclipse.jdt.core.search.SearchMatch; +import org.eclipse.jdt.core.search.SearchPattern; + +public abstract class AbstractPropertiesProvider implements IPropertiesProvider { + + private static final Logger LOGGER = Logger.getLogger(AbstractPropertiesProvider.class.getName()); + + protected abstract String[] getAnnotationNames(); + + public SearchPattern createSearchPattern() { + SearchPattern leftPattern = null; + String[] names = getAnnotationNames(); + for (String name : names) { + if (leftPattern == null) { + leftPattern = createAnnotationSearchPattern(name); + } else { + SearchPattern rightPattern = createAnnotationSearchPattern(name); + if (rightPattern != null) { + leftPattern = SearchPattern.createOrPattern(leftPattern, rightPattern); + } + } + } + return leftPattern; + } + + private static SearchPattern createAnnotationSearchPattern(String annotationName) { + return SearchPattern.createPattern(annotationName, IJavaSearchConstants.ANNOTATION_TYPE, + IJavaSearchConstants.ANNOTATION_TYPE_REFERENCE, SearchPattern.R_EXACT_MATCH); + } + + @Override + public void collectProperties(SearchMatch match, SearchContext context, IPropertiesCollector collector, + IProgressMonitor monitor) { + Object element = match.getElement(); + if (element instanceof IAnnotatable && element instanceof IJavaElement) { + IJavaElement javaElement = (IJavaElement) element; + processAnnotation(javaElement, context, collector, monitor); + } + } + + protected void processAnnotation(IJavaElement javaElement, SearchContext context, IPropertiesCollector collector, + IProgressMonitor monitor) { + try { + String[] names = getAnnotationNames(); + IAnnotation[] annotations = ((IAnnotatable) javaElement).getAnnotations(); + for (IAnnotation annotation : annotations) { + for (String annotationName : names) { + if (isMatchAnnotation(annotation, annotationName)) { + processAnnotation(javaElement, annotation, annotationName, context, collector, monitor); + } + } + } + } catch (Exception e) { + if (LOGGER.isLoggable(Level.SEVERE)) { + LOGGER.log(Level.SEVERE, "Cannot compute MicroProfile properties for the Java element '" + + javaElement.getElementName() + "'.", e); + } + } + } + + protected abstract void processAnnotation(IJavaElement javaElement, IAnnotation annotation, String annotationName, + SearchContext context, IPropertiesCollector collector, IProgressMonitor monitor) throws JavaModelException; + +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/ArtifactResolver.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/ArtifactResolver.java new file mode 100644 index 000000000..38e55239e --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/ArtifactResolver.java @@ -0,0 +1,16 @@ +package com.redhat.microprofile.jdt.core; + +import com.redhat.microprofile.jdt.internal.core.MavenArtifactResolver; + +/** + * Artifact resolver API + * + */ +public interface ArtifactResolver { + + public static final ArtifactResolver DEFAULT_ARTIFACT_RESOLVER = new MavenArtifactResolver(); + + String getArtifact(String groupId, String artifactId, String version); + + String getSources(String groupId, String artifactId, String version); +} \ No newline at end of file diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/IPropertiesCollector.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/IPropertiesCollector.java new file mode 100644 index 000000000..c52870dde --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/IPropertiesCollector.java @@ -0,0 +1,15 @@ +package com.redhat.microprofile.jdt.core; + +import java.util.List; + +import com.redhat.microprofile.commons.MetadataProperty; +import com.redhat.quarkus.commons.EnumItem; + +import io.quarkus.runtime.annotations.ConfigPhase; + +public interface IPropertiesCollector { + + MetadataProperty addMetadataProperty(String referenceType, String propertyName, String type, String defaultValue, + String docs, String location, String extensionName, String source, List enums, + ConfigPhase configPhase); +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/IPropertiesProvider.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/IPropertiesProvider.java new file mode 100644 index 000000000..a3d472fce --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/IPropertiesProvider.java @@ -0,0 +1,34 @@ +package com.redhat.microprofile.jdt.core; + +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.search.SearchMatch; +import org.eclipse.jdt.core.search.SearchPattern; + +import com.redhat.quarkus.jdt.internal.core.QuarkusDeploymentJavaProject.ArtifactResolver; + +public interface IPropertiesProvider { + + default void begin(SearchContext context) { + + } + + default void end(SearchContext context) { + + } + + SearchPattern createSearchPattern(); + + default void contributeToClasspath(IJavaProject project, boolean excludeTestCode, ArtifactResolver artifactResolver, + List newClasspathEntries) throws JavaModelException { + + } + + void collectProperties(SearchMatch match, SearchContext context, IPropertiesCollector collector, + IProgressMonitor monitor); + +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/PropertiesManager.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/PropertiesManager.java new file mode 100644 index 000000000..d92458b0f --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/PropertiesManager.java @@ -0,0 +1,230 @@ +package com.redhat.microprofile.jdt.core; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; + +import org.eclipse.core.resources.IFile; +import org.eclipse.core.runtime.CoreException; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.core.search.IJavaSearchScope; +import org.eclipse.jdt.core.search.SearchEngine; +import org.eclipse.jdt.core.search.SearchMatch; +import org.eclipse.jdt.core.search.SearchParticipant; +import org.eclipse.jdt.core.search.SearchPattern; +import org.eclipse.jdt.core.search.SearchRequestor; +import org.eclipse.jdt.internal.core.JavaModelManager; +import org.eclipse.jdt.internal.core.JavaProject; +import org.eclipse.jdt.internal.core.search.BasicSearchEngine; +import org.eclipse.jdt.internal.core.search.JavaSearchScope; + +import com.redhat.microprofile.commons.MicroProfileProjectInfo; +import com.redhat.microprofile.jdt.internal.core.FakeJavaProject; +import com.redhat.microprofile.jdt.internal.core.MicroProfileConfigPropertyProvider; +import com.redhat.microprofile.jdt.internal.core.MicroProfileRestClientProvider; +import com.redhat.microprofile.jdt.internal.core.PropertiesCollector; +import com.redhat.microprofile.jdt.internal.core.QuarkusConfigPropertiesProvider; +import com.redhat.microprofile.jdt.internal.core.QuarkusConfigRootProvider; +import com.redhat.quarkus.commons.ClasspathKind; +import com.redhat.quarkus.commons.QuarkusPropertiesScope; +import com.redhat.quarkus.jdt.internal.core.QuarkusDeploymentJavaProject; +import com.redhat.quarkus.jdt.internal.core.utils.JDTQuarkusUtils; + +public class PropertiesManager { + + private static final PropertiesManager INSTANCE = new PropertiesManager(); + + private static final Logger LOGGER = Logger.getLogger(PropertiesManager.class.getName()); + + public static PropertiesManager getInstance() { + return INSTANCE; + } + + private final List propertiesProviders; + + public PropertiesManager() { + this.propertiesProviders = new ArrayList<>(); + // TODO : fill properties providers with the propertiesProviders extension point + this.propertiesProviders.add(new MicroProfileConfigPropertyProvider()); + this.propertiesProviders.add(new QuarkusConfigRootProvider()); + this.propertiesProviders.add(new QuarkusConfigPropertiesProvider()); + this.propertiesProviders.add(new MicroProfileRestClientProvider()); + } + + public MicroProfileProjectInfo getMicroProfileProjectInfo(IFile file, QuarkusPropertiesScope[] scopes, + IProgressMonitor progress) throws JavaModelException, CoreException { + String projectName = file.getProject().getName(); + IJavaProject javaProject = JavaModelManager.getJavaModelManager().getJavaModel().getJavaProject(projectName); + ClasspathKind classpathKind = JDTQuarkusUtils.getClasspathKind(file, javaProject); + return getMicroProfileProjectInfo(javaProject, scopes, classpathKind, progress); + } + + public MicroProfileProjectInfo getMicroProfileProjectInfo(IJavaProject javaProject, QuarkusPropertiesScope[] scopes, + ClasspathKind classpathKind, IProgressMonitor monitor) throws JavaModelException, CoreException { + MicroProfileProjectInfo info = createInfo(javaProject, classpathKind); + if (classpathKind == ClasspathKind.NONE) { + info.setProperties(Collections.emptyList()); + return info; + } + long startTime = System.currentTimeMillis(); + if (LOGGER.isLoggable(Level.INFO)) { + LOGGER.info("Start computing MicroProfile properties for '" + info.getProjectURI() + "' project."); + } + PropertiesCollector collector = new PropertiesCollector(); + SearchContext context = new SearchContext(); + try { + beginSearch(context); + // Create pattern + SearchPattern pattern = createSearchPattern(); + SearchEngine engine = new SearchEngine(); + IJavaSearchScope scope = createSearchScope(javaProject, scopes, classpathKind == ClasspathKind.SRC); + engine.search(pattern, new SearchParticipant[] { SearchEngine.getDefaultSearchParticipant() }, scope, + new SearchRequestor() { + + @Override + public void acceptSearchMatch(SearchMatch match) throws CoreException { + collectProperties(match, context, collector, monitor); + } + }, monitor); + info.setProperties(collector.getReferenceProperties()); + } finally { + endSearch(context); + if (LOGGER.isLoggable(Level.INFO)) { + LOGGER.info("End computing MicroProfile properties for '" + info.getProjectURI() + "' project in " + + (System.currentTimeMillis() - startTime) + "ms."); + } + } + return info; + } + + private void beginSearch(SearchContext context) { + for (IPropertiesProvider provider : getPropertiesProviders()) { + provider.begin(context); + } + } + + private void endSearch(SearchContext context) { + for (IPropertiesProvider provider : getPropertiesProviders()) { + provider.end(context); + } + } + + private void collectProperties(SearchMatch match, SearchContext context, PropertiesCollector collector, + IProgressMonitor monitor) { + for (IPropertiesProvider provider : getPropertiesProviders()) { + provider.collectProperties(match, context, collector, monitor); + } + } + + private static MicroProfileProjectInfo createInfo(IJavaProject javaProject, ClasspathKind classpathKind) { + MicroProfileProjectInfo info = new MicroProfileProjectInfo(); + info.setProjectURI(JDTQuarkusUtils.getProjectURI(javaProject)); + info.setClasspathKind(classpathKind); + return info; + } + + private IJavaSearchScope createSearchScope(IJavaProject project, QuarkusPropertiesScope[] scopes, + boolean excludeTestCode) throws JavaModelException { + int scope = scopes[0] == QuarkusPropertiesScope.sources ? IJavaSearchScope.SOURCES + : IJavaSearchScope.SOURCES | IJavaSearchScope.APPLICATION_LIBRARIES; + List newClasspathEntries = new ArrayList<>(); + for (IPropertiesProvider provider : getPropertiesProviders()) { + provider.contributeToClasspath(project, excludeTestCode, + QuarkusDeploymentJavaProject.DEFAULT_ARTIFACT_RESOLVER, newClasspathEntries); + } + if (!newClasspathEntries.isEmpty()) { + FakeJavaProject fakeProject = new FakeJavaProject(project, + newClasspathEntries.toArray(new IClasspathEntry[newClasspathEntries.size()])); + return createJavaSearchScope(fakeProject, excludeTestCode, fakeProject.getElementsToSearch(scopes[0]), + scope); + } + return BasicSearchEngine.createJavaSearchScope(excludeTestCode, new IJavaElement[] { project }); + } + + private SearchPattern createSearchPattern() { + SearchPattern leftPattern = null; + for (IPropertiesProvider provider : getPropertiesProviders()) { + if (leftPattern == null) { + leftPattern = provider.createSearchPattern(); + } else { + SearchPattern rightPattern = provider.createSearchPattern(); + if (rightPattern != null) { + leftPattern = SearchPattern.createOrPattern(leftPattern, rightPattern); + } + } + } + return leftPattern; + } + + /** + * This code is the same than + * {@link BasicSearchEngine#createJavaSearchScope(boolean, IJavaElement[], boolean)}. + * It overrides {@link JavaSearchScope#packageFragmentRoot(String, int, String)} + * to search the first the package root (JAR) from the given fake project. + * + * @param fakeProject + * @param excludeTestCode + * @param elements + * @param includeMask + * @return + */ + private static IJavaSearchScope createJavaSearchScope(IJavaProject fakeProject, boolean excludeTestCode, + IJavaElement[] elements, int includeMask) { + HashSet projectsToBeAdded = new HashSet(2); + for (int i = 0, length = elements.length; i < length; i++) { + IJavaElement element = elements[i]; + if (element instanceof JavaProject) { + projectsToBeAdded.add(element); + } + } + JavaSearchScope scope = new JavaSearchScope(excludeTestCode) { + + @Override + public IPackageFragmentRoot packageFragmentRoot(String resourcePathString, int jarSeparatorIndex, + String jarPath) { + // Search at first in the fake project the package root to avoid creating a non + // existing IProject (because fake project doesn't exists) + try { + IPackageFragmentRoot[] roots = fakeProject.getPackageFragmentRoots(); + for (IPackageFragmentRoot root : roots) { + if (resourcePathString.startsWith(root.getPath().toOSString())) { + return root; + } + } + } catch (JavaModelException e) { + // ignore + } + // Not found... + return super.packageFragmentRoot(resourcePathString, jarSeparatorIndex, jarPath); + } + }; + for (int i = 0, length = elements.length; i < length; i++) { + IJavaElement element = elements[i]; + if (element != null) { + try { + if (projectsToBeAdded.contains(element)) { + scope.add((JavaProject) element, includeMask, projectsToBeAdded); + } else { + scope.add(element); + } + } catch (JavaModelException e) { + // ignore + } + } + } + return scope; + } + + List getPropertiesProviders() { + return propertiesProviders; + } + +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/SearchContext.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/SearchContext.java new file mode 100644 index 000000000..7d7e382e6 --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/SearchContext.java @@ -0,0 +1,22 @@ +package com.redhat.microprofile.jdt.core; + +import java.util.HashMap; +import java.util.Map; + +public class SearchContext { + + private final Map cache; + + public SearchContext() { + cache = new HashMap<>(); + } + + public void put(String key, Object value) { + cache.put(key, value); + } + + public Object get(String key) { + return cache.get(key); + } + +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/utils/JDTTypeUtils.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/utils/JDTTypeUtils.java new file mode 100644 index 000000000..8f4794cb9 --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/core/utils/JDTTypeUtils.java @@ -0,0 +1,109 @@ +package com.redhat.microprofile.jdt.core.utils; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.eclipse.jdt.core.IField; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.corext.util.JavaModelUtil; + +import com.redhat.quarkus.commons.EnumItem; + +public class JDTTypeUtils { + + private static final List NUMBER_TYPES = Arrays.asList("short", "int", "long", "double", "float"); + + public static IType findType(IJavaProject project, String name) { + try { + return project.findType(name); + } catch (JavaModelException e) { + return null; + } + } + + public static String getResolvedTypeName(IField field) { + try { + String signature = field.getTypeSignature(); + IType primaryType = field.getTypeRoot().findPrimaryType(); + return JavaModelUtil.getResolvedTypeName(signature, primaryType); + } catch (JavaModelException e) { + return null; + } + } + + public static String getResolvedResultTypeName(IMethod method) { + try { + String signature = method.getReturnType(); + IType primaryType = method.getTypeRoot().findPrimaryType(); + return JavaModelUtil.getResolvedTypeName(signature, primaryType); + } catch (JavaModelException e) { + return null; + } + } + + public static String getPropertyType(IType type, String typeName) { + return type != null ? type.getFullyQualifiedName() : typeName; + } + + public static String getPropertySource(IField field) { + // field and class source + return field.getDeclaringType().getFullyQualifiedName() + "#" + field.getElementName(); + } + + public static String getPropertySource(IMethod method) throws JavaModelException { + return method.getDeclaringType().getFullyQualifiedName() + "#" + method.getElementName() + + method.getSignature(); + } + + public static List getPropertyEnumerations(IType fieldClass) throws JavaModelException { + List enumerations = null; + if (fieldClass != null && fieldClass.isEnum()) { + enumerations = new ArrayList<>(); + IJavaElement[] children = fieldClass.getChildren(); + for (IJavaElement c : children) { + if (c.getElementType() == IJavaElement.FIELD && ((IField) c).isEnumConstant()) { + String enumName = ((IField) c).getElementName(); + // TODO: extract Javadoc + String enumDocs = null; + enumerations.add(new EnumItem(enumName, enumDocs)); + } + } + } + return enumerations; + } + + public static boolean isOptional(String fieldTypeName) { + return fieldTypeName.startsWith("java.util.Optional"); + } + + public static String[] getRawTypeParameters(String fieldTypeName) { + int start = fieldTypeName.indexOf("<") + 1; + int end = fieldTypeName.lastIndexOf(">"); + String keyValue = fieldTypeName.substring(start, end); + int index = keyValue.indexOf(','); + return new String[] { keyValue.substring(0, index), keyValue.substring(index + 1, keyValue.length()) }; + } + + public static boolean isPrimitiveType(String valueClass) { + return valueClass.equals("java.lang.String") || valueClass.equals("java.lang.Boolean") + || valueClass.equals("java.lang.Integer") || valueClass.equals("java.lang.Long") + || valueClass.equals("java.lang.Double") || valueClass.equals("java.lang.Float"); + } + + public static boolean isMap(String mapValueClass) { + return mapValueClass.startsWith("java.util.Map"); + } + + public static boolean isList(String valueClass) { + return valueClass.startsWith("java.util.List"); + } + + public static boolean isNumber(String valueClass) { + return NUMBER_TYPES.contains(valueClass); + } +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/FakeJavaProject.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/FakeJavaProject.java new file mode 100644 index 000000000..7f1531750 --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/FakeJavaProject.java @@ -0,0 +1,52 @@ +package com.redhat.microprofile.jdt.internal.core; + +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.JavaModelException; +import org.eclipse.jdt.internal.core.ExternalJavaProject; + +import com.redhat.quarkus.commons.QuarkusPropertiesScope; + +public class FakeJavaProject extends ExternalJavaProject { + + private final IJavaProject rootProject; + + public FakeJavaProject(IJavaProject rootProject, IClasspathEntry[] entries) throws JavaModelException { + super(entries); + this.rootProject = rootProject; + } + + /** + * Returns the java elements to search according the scope: + * + *
    + *
  • sources scope: only Quarkus Java project
  • + *
  • classpath scope: + *
      + *
    • the Quarkus project
    • + *
    • all deployment JARs
    • + *
    + *
  • + *
+ * + * @param propertiesScope + * + * @return the java elements to search + * @throws JavaModelException + */ + public IJavaElement[] getElementsToSearch(QuarkusPropertiesScope propertiesScope) throws JavaModelException { + if (propertiesScope == QuarkusPropertiesScope.sources) { + return new IJavaElement[] { rootProject }; + } + IPackageFragmentRoot[] roots = super.getPackageFragmentRoots(); + IJavaElement[] elements = new IJavaElement[1 + roots.length]; + elements[0] = rootProject; + for (int i = 0; i < roots.length; i++) { + elements[i + 1] = roots[i]; + } + return elements; + } + +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/MavenArtifactResolver.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/MavenArtifactResolver.java new file mode 100644 index 000000000..39a52deb1 --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/MavenArtifactResolver.java @@ -0,0 +1,55 @@ +/******************************************************************************* +* Copyright (c) 2019 Red Hat Inc. and others. +* All rights reserved. This program and the accompanying materials +* which accompanies this distribution, and is available at +* http://www.eclipse.org/legal/epl-v20.html +* +* Contributors: +* Red Hat Inc. - initial API and implementation +*******************************************************************************/ +package com.redhat.microprofile.jdt.internal.core; + +import java.io.File; +import java.util.logging.Level; +import java.util.logging.Logger; + +import com.redhat.microprofile.jdt.core.ArtifactResolver; +import com.redhat.quarkus.jdt.internal.core.utils.DependencyUtil; + +/** + * Maven artifact resolver used to download JAR and JAR sources with maven. + * + * @author Angelo ZERR + * + */ +public class MavenArtifactResolver implements ArtifactResolver { + + private static final Logger LOGGER = Logger.getLogger(MavenArtifactResolver.class.getName()); + + @Override + public String getArtifact(String groupId, String artifactId, String version) { + File jarFile = null; + try { + jarFile = DependencyUtil.getArtifact(groupId, artifactId, version, null); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Maven artifact JAR (groupId=" + groupId + ", artifactId=" + artifactId + + ", version=" + version + ") download failed.", e); + return null; + } + return jarFile != null ? jarFile.toString() : null; + } + + @Override + public String getSources(String groupId, String artifactId, String version) { + File jarFile = null; + try { + jarFile = DependencyUtil.getSources(groupId, artifactId, version); + } catch (Exception e) { + LOGGER.log(Level.SEVERE, "Maven artifact JAR Sources (groupId=" + groupId + ", artifactId=" + artifactId + + ", version=" + version + ") download failed.", e); + return null; + } + return jarFile != null ? jarFile.toString() : null; + } + +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/MicroProfileConfigPropertyProvider.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/MicroProfileConfigPropertyProvider.java new file mode 100644 index 000000000..9ca666b8e --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/MicroProfileConfigPropertyProvider.java @@ -0,0 +1,68 @@ +package com.redhat.microprofile.jdt.internal.core; + +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.findType; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getPropertyEnumerations; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getPropertySource; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getPropertyType; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getResolvedTypeName; +import static com.redhat.quarkus.jdt.internal.core.QuarkusConstants.CONFIG_PROPERTY_ANNOTATION; +import static com.redhat.quarkus.jdt.internal.core.utils.AnnotationUtils.getAnnotationMemberValue; + +import java.util.List; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.IAnnotation; +import org.eclipse.jdt.core.IField; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; + +import com.redhat.microprofile.commons.MicroProfileProjectInfo; +import com.redhat.microprofile.jdt.core.AbstractPropertiesProvider; +import com.redhat.microprofile.jdt.core.IPropertiesCollector; +import com.redhat.microprofile.jdt.core.SearchContext; +import com.redhat.quarkus.commons.EnumItem; + +import io.quarkus.runtime.annotations.ConfigPhase; + +public class MicroProfileConfigPropertyProvider extends AbstractPropertiesProvider { + + private static final String[] ANNOTATION_NAMES = { CONFIG_PROPERTY_ANNOTATION }; + + @Override + protected String[] getAnnotationNames() { + return ANNOTATION_NAMES; + } + + @Override + protected void processAnnotation(IJavaElement javaElement, IAnnotation configPropertyAnnotation, + String annotationName, SearchContext context, IPropertiesCollector collector, IProgressMonitor monitor) + throws JavaModelException { + if (javaElement.getElementType() == IJavaElement.FIELD) { + String propertyName = getAnnotationMemberValue(configPropertyAnnotation, "name"); + if (propertyName != null && !propertyName.isEmpty()) { + IField field = (IField) javaElement; + String fieldTypeName = getResolvedTypeName(field); + IType fieldClass = findType(field.getJavaProject(), fieldTypeName); + String defaultValue = getAnnotationMemberValue(configPropertyAnnotation, "defaultValue"); + // Location (JAR, src) + IPackageFragmentRoot packageRoot = (IPackageFragmentRoot) javaElement + .getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT); + String location = packageRoot.getPath().toString(); + + String extensionName = null; + ConfigPhase configPhase = null; + String type = getPropertyType(fieldClass, fieldTypeName); + String docs = null; + String source = getPropertySource(field); + List enums = getPropertyEnumerations(fieldClass); + + collector.addMetadataProperty(MicroProfileProjectInfo.DEFAULT_REFERENCE_TYPE, propertyName, type, + defaultValue, docs, location, extensionName, source, enums, configPhase); + } + } + + } + +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/MicroProfileRestClientProvider.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/MicroProfileRestClientProvider.java new file mode 100644 index 000000000..9065e2941 --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/MicroProfileRestClientProvider.java @@ -0,0 +1,61 @@ +package com.redhat.microprofile.jdt.internal.core; + +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.jdt.core.IAnnotation; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaModelException; + +import com.redhat.microprofile.commons.MicroProfileProjectInfo; +import com.redhat.microprofile.jdt.core.AbstractPropertiesProvider; +import com.redhat.microprofile.jdt.core.IPropertiesCollector; +import com.redhat.microprofile.jdt.core.SearchContext; + +public class MicroProfileRestClientProvider extends AbstractPropertiesProvider { + + private static final String[] ANNOTATION_NAMES = { + "org.eclipse.microprofile.rest.client.inject.RegisterRestClient" }; + + private static final String MP_REST_CLASS_REFERENCE_TYPE = "${mp-rest-class}"; + + private static final String MP_REST_ADDED = MicroProfileRestClientProvider.class.getName() + "#mp-rest"; + + @Override + protected String[] getAnnotationNames() { + return ANNOTATION_NAMES; + } + + @Override + protected void processAnnotation(IJavaElement javaElement, IAnnotation configPropertyAnnotation, + String annotationName, SearchContext context, IPropertiesCollector collector, IProgressMonitor monitor) + throws JavaModelException { + if (javaElement.getElementType() == IJavaElement.TYPE) { + if (context.get(MP_REST_ADDED) == null) { + // /mp-rest/url + String docs = "The base URL to use for this service,\r\n" + + "the equivalent of the baseUrl method. This property is considered required, however\r\n" + + "implementations may have other ways to define these URLs.\r\n" + ""; + collector.addMetadataProperty(MicroProfileProjectInfo.DEFAULT_REFERENCE_TYPE, + MP_REST_CLASS_REFERENCE_TYPE + "/mp-rest/url", "java.lang.String", null, docs, null, null, null, + null, null); + // /mp-rest/scope + docs = "The fully qualified classname to a\r\n" + + "CDI scope to use for injection, defaults to javax.enterprise.context.Dependent as mentioned\r\n" + + "above."; + collector.addMetadataProperty(MicroProfileProjectInfo.DEFAULT_REFERENCE_TYPE, + MP_REST_CLASS_REFERENCE_TYPE + "/mp-rest/scope", "java.lang.String", + "javax.enterprise.context.Dependent", docs, null, null, null, null, null); + context.put(MP_REST_ADDED, Boolean.TRUE); + } + IType type = (IType) javaElement; + String docs = null; + String location = null; + String source = type.getFullyQualifiedName(); + collector.addMetadataProperty(MP_REST_CLASS_REFERENCE_TYPE, type.getFullyQualifiedName(), + type.getFullyQualifiedName(), null, docs, location, null, source, null, null); + + } + + } + +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/PropertiesCollector.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/PropertiesCollector.java new file mode 100644 index 000000000..c5710edaf --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/PropertiesCollector.java @@ -0,0 +1,77 @@ +package com.redhat.microprofile.jdt.internal.core; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.redhat.microprofile.commons.MetadataProperty; +import com.redhat.microprofile.commons.ReferenceProperties; +import com.redhat.microprofile.jdt.core.IPropertiesCollector; +import com.redhat.quarkus.commons.EnumItem; +import com.redhat.quarkus.commons.ExtendedConfigDescriptionBuildItem; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; + +public class PropertiesCollector implements IPropertiesCollector { + + private final Map referenceProperties; + + public PropertiesCollector() { + this.referenceProperties = new HashMap<>(); + } + + @Override + public MetadataProperty addMetadataProperty(String referenceType, String propertyName, String type, + String defaultValue, String docs, String location, String extensionName, String source, + List enums, ConfigPhase configPhase) { + + if (ConfigItem.NO_DEFAULT.equals(defaultValue)) { + defaultValue = null; + } + MetadataProperty property = new MetadataProperty(); + property.setPropertyName(propertyName); + property.setType(type); + property.setDefaultValue(defaultValue); + property.setDocs(docs); + + // Extra properties + + property.setExtensionName(extensionName); + property.setLocation(location); + property.setSource(source); + if (configPhase != null) { + property.setPhase(getPhase(configPhase)); + } + property.setRequired(defaultValue == null); + property.setEnums(enums); + + ReferenceProperties reference = referenceProperties.get(referenceType); + if (reference == null) { + reference = new ReferenceProperties(); + reference.setType(referenceType); + reference.setProperties(new ArrayList<>()); + referenceProperties.put(referenceType, reference); + } + reference.getProperties().add(property); + return property; + } + + private static int getPhase(ConfigPhase configPhase) { + switch (configPhase) { + case BUILD_TIME: + return ExtendedConfigDescriptionBuildItem.CONFIG_PHASE_BUILD_TIME; + case BUILD_AND_RUN_TIME_FIXED: + return ExtendedConfigDescriptionBuildItem.CONFIG_PHASE_BUILD_AND_RUN_TIME_FIXED; + case RUN_TIME: + return ExtendedConfigDescriptionBuildItem.CONFIG_PHASE_RUN_TIME; + default: + return ExtendedConfigDescriptionBuildItem.CONFIG_PHASE_BUILD_TIME; + } + } + + public List getReferenceProperties() { + return new ArrayList<>(referenceProperties.values()); + } +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/QuarkusConfigPropertiesProvider.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/QuarkusConfigPropertiesProvider.java new file mode 100644 index 000000000..98fe1e6d0 --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/QuarkusConfigPropertiesProvider.java @@ -0,0 +1,361 @@ +package com.redhat.microprofile.jdt.internal.core; + +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.findType; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getPropertyEnumerations; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getPropertySource; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getPropertyType; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getResolvedResultTypeName; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getResolvedTypeName; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.isList; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.isMap; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.isOptional; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.isPrimitiveType; +import static com.redhat.quarkus.jdt.internal.core.QuarkusConstants.CONFIG_PROPERTY_ANNOTATION; +import static com.redhat.quarkus.jdt.internal.core.QuarkusConstants.QUARKUS_EXTENSION_PROPERTIES; +import static com.redhat.quarkus.jdt.internal.core.utils.AnnotationUtils.getAnnotation; +import static com.redhat.quarkus.jdt.internal.core.utils.AnnotationUtils.getAnnotationMemberValue; +import static io.quarkus.runtime.util.StringUtil.camelHumpsIterator; +import static io.quarkus.runtime.util.StringUtil.join; +import static io.quarkus.runtime.util.StringUtil.lowerCase; +import static io.quarkus.runtime.util.StringUtil.withoutSuffix; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Properties; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.Flags; +import org.eclipse.jdt.core.IAnnotation; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IField; +import org.eclipse.jdt.core.IJarEntryResource; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IMethod; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.ITypeHierarchy; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; + +import com.redhat.microprofile.commons.MicroProfileProjectInfo; +import com.redhat.microprofile.jdt.core.AbstractPropertiesProvider; +import com.redhat.microprofile.jdt.core.IPropertiesCollector; +import com.redhat.microprofile.jdt.core.SearchContext; +import com.redhat.quarkus.commons.EnumItem; +import com.redhat.quarkus.jdt.internal.core.QuarkusDeploymentJavaProject.ArtifactResolver; +import com.redhat.quarkus.jdt.internal.core.utils.JDTQuarkusSearchUtils; +import com.redhat.quarkus.jdt.internal.core.utils.JDTQuarkusUtils; + +import io.quarkus.arc.config.ConfigProperties; +import io.quarkus.deployment.bean.JavaBeanUtil; + +public class QuarkusConfigPropertiesProvider extends AbstractPropertiesProvider { + + private static final Logger LOGGER = Logger.getLogger(QuarkusConfigPropertiesProvider.class.getName()); + + private static final String[] ANNOTATION_NAMES = { "io.quarkus.arc.config.ConfigProperties" }; + + @Override + protected String[] getAnnotationNames() { + return ANNOTATION_NAMES; + } + + @Override + public void contributeToClasspath(IJavaProject project, boolean excludeTestCode, ArtifactResolver artifactResolver, + List deploymentJarEntries) throws JavaModelException { + IClasspathEntry[] entries = project.getResolvedClasspath(true); + List existingJars = Stream.of(entries) + // filter entry to collect only JAR + .filter(entry -> entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) + // filter Quarkus deployment JAR marked as test scope. Ex: + // 'quarkus-core-deployment' can be marked as test scope, we must exclude them + // to avoid to ignore it in the next step. + .filter(entry -> !excludeTestCode || (excludeTestCode && !entry.isTest())) // + .map(entry -> entry.getPath().lastSegment()).collect(Collectors.toList()); + for (IClasspathEntry entry : entries) { + if (excludeTestCode && entry.isTest()) { + continue; + } + switch (entry.getEntryKind()) { + + case IClasspathEntry.CPE_LIBRARY: + + try { + String jarPath = entry.getPath().toOSString(); + IPackageFragmentRoot root = project.getPackageFragmentRoot(jarPath); + if (root != null) { + IJarEntryResource resource = JDTQuarkusSearchUtils.findPropertiesResource(root, + QUARKUS_EXTENSION_PROPERTIES); + if (resource != null) { + Properties properties = new Properties(); + properties.load(resource.getContents()); + // deployment-artifact=io.quarkus\:quarkus-undertow-deployment\:0.21.1 + String deploymentArtifact = properties.getProperty("deployment-artifact"); + String[] result = deploymentArtifact.split(":"); + String groupId = result[0]; + String artifactId = result[1]; + String version = result[2]; + // Get or download deployment JAR + String deploymentJarFile = artifactResolver.getArtifact(groupId, artifactId, version); + if (deploymentJarFile != null) { + IPath deploymentJarFilePath = new Path(deploymentJarFile); + String deploymentJarName = deploymentJarFilePath.lastSegment(); + if (!existingJars.contains(deploymentJarName)) { + // The *-deployment JAR is not included in the classpath project, add it. + existingJars.add(deploymentJarName); + IPath sourceAttachmentPath = null; + // Get or download deployment sources JAR + String sourceJarFile = artifactResolver.getSources(groupId, artifactId, version); + if (sourceJarFile != null) { + sourceAttachmentPath = new Path(sourceJarFile); + } + deploymentJarEntries.add(JavaCore.newLibraryEntry(deploymentJarFilePath, + sourceAttachmentPath, null)); + } + } + } + } + } catch (Exception e) { + // do nothing + } + + break; + } + } + // Add the Quarkus project in classpath to resolve dependencies of deployment + // Quarkus JARs. + deploymentJarEntries.add(JavaCore.newProjectEntry(project.getProject().getLocation())); + } + + @Override + protected void processAnnotation(IJavaElement javaElement, IAnnotation annotation, String annotationName, + SearchContext context, IPropertiesCollector collector, IProgressMonitor monitor) throws JavaModelException { + processConfigProperties(javaElement, annotation, collector, monitor); + } + + // ------------- Process Quarkus ConfigProperties ------------- + + private static void processConfigProperties(IJavaElement javaElement, IAnnotation configPropertiesAnnotation, + IPropertiesCollector collector, IProgressMonitor monitor) throws JavaModelException { + if (javaElement.getElementType() != IJavaElement.TYPE) { + return; + } + IType configPropertiesType = (IType) javaElement; + // Location (JAR, src) + IPackageFragmentRoot packageRoot = (IPackageFragmentRoot) javaElement + .getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT); + String location = packageRoot.getPath().toString(); + // Quarkus Extension name + String extensionName = JDTQuarkusUtils.getExtensionName(location); + + String prefix = determinePrefix(configPropertiesType, configPropertiesAnnotation); + if (configPropertiesType.isInterface()) { + // See + // https://github.com/quarkusio/quarkus/blob/0796d712d9a3cf8251d9d8808b705f1a04032ee2/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/InterfaceConfigPropertiesUtil.java#L89 + List allInterfaces = new ArrayList<>(Arrays.asList(findInterfaces(configPropertiesType, monitor))); + allInterfaces.add(0, configPropertiesType); + + for (IType configPropertiesInterface : allInterfaces) { + // Loop for each methods. + IJavaElement[] elements = configPropertiesInterface.getChildren(); + // Loop for each fields. + for (IJavaElement child : elements) { + if (child.getElementType() == IJavaElement.METHOD) { + IMethod method = (IMethod) child; + if (Flags.isDefaultMethod(method.getFlags())) { // don't do anything with default methods + continue; + } + if (method.getNumberOfParameters() > 0) { + LOGGER.log(Level.INFO, + "Method " + method.getElementName() + " of interface " + + method.getDeclaringType().getFullyQualifiedName() + + " is not a getter method since it defined parameters"); + continue; + } + if ("Void()".equals(method.getReturnType())) { + LOGGER.log(Level.INFO, + "Method " + method.getElementName() + " of interface " + + method.getDeclaringType().getFullyQualifiedName() + + " is not a getter method since it returns void"); + continue; + } + String name = null; + String defaultValue = null; + IAnnotation configPropertyAnnotation = getAnnotation(method, CONFIG_PROPERTY_ANNOTATION); + if (configPropertyAnnotation != null) { + name = getAnnotationMemberValue(configPropertyAnnotation, "name"); + defaultValue = getAnnotationMemberValue(configPropertyAnnotation, "defaultValue"); + } + if (name == null) { + name = getPropertyNameFromMethodName(method); + } + if (name == null) { + continue; + } + + String propertyName = prefix + "." + name; + String methodResultTypeName = getResolvedResultTypeName(method); + IType returnType = findType(method.getJavaProject(), methodResultTypeName); + + // Method result type + String type = getPropertyType(returnType, methodResultTypeName); + + // TODO: extract Javadoc from Java sources + String docs = null; + + // Method source + String source = getPropertySource(method); + + // Enumerations + List enumerations = getPropertyEnumerations(returnType); + + if (isSimpleFieldType(returnType, methodResultTypeName)) { + collector.addMetadataProperty(MicroProfileProjectInfo.DEFAULT_REFERENCE_TYPE, propertyName, + type, defaultValue, docs, location, extensionName, source, enumerations, null); + } else { + populateConfigObject(returnType, propertyName, location, extensionName, new HashSet<>(), + collector, monitor); + } + + } + } + } + } else { + // See + // https://github.com/quarkusio/quarkus/blob/e8606513e1bd14f0b1aaab7f9969899bd27c55a3/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ClassConfigPropertiesUtil.java#L117 + // TODO : validation + populateConfigObject(configPropertiesType, prefix, location, extensionName, new HashSet<>(), collector, + monitor); + } + } + + private static boolean isSimpleFieldType(IType type, String typeName) { + return type == null || isPrimitiveType(typeName) || isList(typeName) || isMap(typeName) || isOptional(typeName); + } + + private static IType[] findInterfaces(IType type, IProgressMonitor progressMonitor) throws JavaModelException { + ITypeHierarchy typeHierarchy = type.newSupertypeHierarchy(progressMonitor); + return typeHierarchy.getAllSuperInterfaces(type); + } + + private static void populateConfigObject(IType configPropertiesType, String prefixStr, final String location, + String extensionName, Set typesAlreadyProcessed, IPropertiesCollector collector, + IProgressMonitor monitor) throws JavaModelException { + if (typesAlreadyProcessed.contains(configPropertiesType)) { + return; + } + typesAlreadyProcessed.add(configPropertiesType); + IJavaElement[] elements = configPropertiesType.getChildren(); + // Loop for each fields. + for (IJavaElement child : elements) { + if (child.getElementType() == IJavaElement.FIELD) { + // The following code is an adaptation for JDT of + // Quarkus arc code: + // https://github.com/quarkusio/quarkus/blob/e8606513e1bd14f0b1aaab7f9969899bd27c55a3/extensions/arc/deployment/src/main/java/io/quarkus/arc/deployment/configproperties/ClassConfigPropertiesUtil.java#L211 + IField field = (IField) child; + boolean useFieldAccess = false; + String setterName = JavaBeanUtil.getSetterName(field.getElementName()); + String configClassInfo = configPropertiesType.getFullyQualifiedName(); + IMethod setter = findMethod(configPropertiesType, setterName, field.getTypeSignature()); + if (setter == null) { + if (!Flags.isPublic(field.getFlags()) || Flags.isFinal(field.getFlags())) { + LOGGER.log(Level.INFO, + "Configuration properties class " + configClassInfo + + " does not have a setter for field " + field + + " nor is the field a public non-final field"); + continue; + } + useFieldAccess = true; + } + if (!useFieldAccess && !Flags.isPublic(setter.getFlags())) { + LOGGER.log(Level.INFO, "Setter " + setterName + " of class " + configClassInfo + " must be public"); + continue; + } + + String name = field.getElementName(); + // The default value is managed with assign like : 'public String suffix = "!"'; + // Getting "!" value is possible but it requires to re-parse the Java file to + // build a DOM CompilationUnit to extract assigned value. + final String defaultValue = null; + String propertyName = prefixStr + "." + name; + + String fieldTypeName = getResolvedTypeName(field); + IType fieldClass = findType(field.getJavaProject(), fieldTypeName); + if (isSimpleFieldType(fieldClass, fieldTypeName)) { + + // Class type + String type = getPropertyType(fieldClass, fieldTypeName); + + // Javadoc + String docs = null; + + // field and class source + String source = getPropertySource(field); + + // Enumerations + List enumerations = getPropertyEnumerations(fieldClass); + + collector.addMetadataProperty(MicroProfileProjectInfo.DEFAULT_REFERENCE_TYPE, propertyName, type, + defaultValue, docs, location, extensionName, source, enumerations, null); + } else { + populateConfigObject(fieldClass, propertyName, location, extensionName, typesAlreadyProcessed, + collector, monitor); + } + } + } + } + + private static String getPropertyNameFromMethodName(IMethod method) { + try { + return JavaBeanUtil.getPropertyNameFromGetter(method.getElementName()); + } catch (IllegalArgumentException e) { + LOGGER.log(Level.INFO, "Method " + method.getElementName() + " of interface " + + method.getDeclaringType().getElementName() + + " is not a getter method. Either rename the method to follow getter name conventions or annotate the method with @ConfigProperty"); + return null; + } + } + + private static IMethod findMethod(IType configPropertiesType, String setterName, String fieldTypeSignature) { + IMethod method = configPropertiesType.getMethod(setterName, new String[] { fieldTypeSignature }); + return method.exists() ? method : null; + } + + private static String determinePrefix(IType configPropertiesType, IAnnotation configPropertiesAnnotation) + throws JavaModelException { + String fromAnnotation = getPrefixFromAnnotation(configPropertiesAnnotation); + if (fromAnnotation != null) { + return fromAnnotation; + } + return getPrefixFromClassName(configPropertiesType); + } + + private static String getPrefixFromAnnotation(IAnnotation configPropertiesAnnotation) throws JavaModelException { + String value = getAnnotationMemberValue(configPropertiesAnnotation, "prefix"); + if (value == null) { + return null; + } + if (ConfigProperties.UNSET_PREFIX.equals(value) || value.isEmpty()) { + return null; + } + return value; + } + + private static String getPrefixFromClassName(IType className) { + String simpleName = className.getElementName(); // className.isInner() ? className.local() : + // className.withoutPackagePrefix(); + return join("-", withoutSuffix(lowerCase(camelHumpsIterator(simpleName)), "config", "configuration", + "properties", "props")); + } + +} diff --git a/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/QuarkusConfigRootProvider.java b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/QuarkusConfigRootProvider.java new file mode 100644 index 000000000..4253ab5ab --- /dev/null +++ b/quarkus.jdt/com.redhat.quarkus.jdt.core/src/main/java/com/redhat/microprofile/jdt/internal/core/QuarkusConfigRootProvider.java @@ -0,0 +1,507 @@ +package com.redhat.microprofile.jdt.internal.core; + +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.findType; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getPropertyEnumerations; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getPropertySource; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getPropertyType; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.getResolvedTypeName; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.isList; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.isMap; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.isNumber; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.isOptional; +import static com.redhat.microprofile.jdt.core.utils.JDTTypeUtils.isPrimitiveType; +import static com.redhat.quarkus.jdt.internal.core.QuarkusConstants.CONFIG_GROUP_ANNOTATION; +import static com.redhat.quarkus.jdt.internal.core.QuarkusConstants.CONFIG_ITEM_ANNOTATION; +import static com.redhat.quarkus.jdt.internal.core.QuarkusConstants.QUARKUS_EXTENSION_PROPERTIES; +import static com.redhat.quarkus.jdt.internal.core.QuarkusConstants.QUARKUS_JAVADOC_PROPERTIES; +import static com.redhat.quarkus.jdt.internal.core.QuarkusConstants.QUARKUS_PREFIX; +import static com.redhat.quarkus.jdt.internal.core.utils.AnnotationUtils.getAnnotation; +import static com.redhat.quarkus.jdt.internal.core.utils.AnnotationUtils.getAnnotationMemberValue; +import static io.quarkus.runtime.util.StringUtil.camelHumpsIterator; +import static io.quarkus.runtime.util.StringUtil.hyphenate; +import static io.quarkus.runtime.util.StringUtil.join; +import static io.quarkus.runtime.util.StringUtil.lowerCase; +import static io.quarkus.runtime.util.StringUtil.lowerCaseFirst; +import static io.quarkus.runtime.util.StringUtil.withoutSuffix; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import org.eclipse.core.runtime.IPath; +import org.eclipse.core.runtime.IProgressMonitor; +import org.eclipse.core.runtime.Path; +import org.eclipse.jdt.core.IAnnotatable; +import org.eclipse.jdt.core.IAnnotation; +import org.eclipse.jdt.core.IClasspathEntry; +import org.eclipse.jdt.core.IField; +import org.eclipse.jdt.core.IJarEntryResource; +import org.eclipse.jdt.core.IJavaElement; +import org.eclipse.jdt.core.IJavaProject; +import org.eclipse.jdt.core.IPackageFragmentRoot; +import org.eclipse.jdt.core.ISourceRange; +import org.eclipse.jdt.core.IType; +import org.eclipse.jdt.core.JavaCore; +import org.eclipse.jdt.core.JavaModelException; + +import com.redhat.microprofile.commons.MetadataProperty; +import com.redhat.microprofile.commons.MicroProfileProjectInfo; +import com.redhat.microprofile.jdt.core.AbstractPropertiesProvider; +import com.redhat.microprofile.jdt.core.IPropertiesCollector; +import com.redhat.microprofile.jdt.core.SearchContext; +import com.redhat.quarkus.commons.EnumItem; +import com.redhat.quarkus.jdt.internal.core.QuarkusDeploymentJavaProject.ArtifactResolver; +import com.redhat.quarkus.jdt.internal.core.utils.JDTQuarkusSearchUtils; +import com.redhat.quarkus.jdt.internal.core.utils.JDTQuarkusUtils; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; + +public class QuarkusConfigRootProvider extends AbstractPropertiesProvider { + + private static final String[] ANNOTATION_NAMES = { "io.quarkus.runtime.annotations.ConfigRoot" }; + + private static final String JAVADOC_CACHE_KEY = QuarkusConfigRootProvider.class.getName() + "#javadoc"; + + @Override + protected String[] getAnnotationNames() { + return ANNOTATION_NAMES; + } + + @Override + public void begin(SearchContext context) { + Map javadocCache = new HashMap<>(); + context.put(JAVADOC_CACHE_KEY, javadocCache); + } + + @Override + public void contributeToClasspath(IJavaProject project, boolean excludeTestCode, ArtifactResolver artifactResolver, + List deploymentJarEntries) throws JavaModelException { + IClasspathEntry[] entries = project.getResolvedClasspath(true); + List existingJars = Stream.of(entries) + // filter entry to collect only JAR + .filter(entry -> entry.getEntryKind() == IClasspathEntry.CPE_LIBRARY) + // filter Quarkus deployment JAR marked as test scope. Ex: + // 'quarkus-core-deployment' can be marked as test scope, we must exclude them + // to avoid to ignore it in the next step. + .filter(entry -> !excludeTestCode || (excludeTestCode && !entry.isTest())) // + .map(entry -> entry.getPath().lastSegment()).collect(Collectors.toList()); + for (IClasspathEntry entry : entries) { + if (excludeTestCode && entry.isTest()) { + continue; + } + switch (entry.getEntryKind()) { + + case IClasspathEntry.CPE_LIBRARY: + + try { + String jarPath = entry.getPath().toOSString(); + IPackageFragmentRoot root = project.getPackageFragmentRoot(jarPath); + if (root != null) { + IJarEntryResource resource = JDTQuarkusSearchUtils.findPropertiesResource(root, + QUARKUS_EXTENSION_PROPERTIES); + if (resource != null) { + Properties properties = new Properties(); + properties.load(resource.getContents()); + // deployment-artifact=io.quarkus\:quarkus-undertow-deployment\:0.21.1 + String deploymentArtifact = properties.getProperty("deployment-artifact"); + String[] result = deploymentArtifact.split(":"); + String groupId = result[0]; + String artifactId = result[1]; + String version = result[2]; + // Get or download deployment JAR + String deploymentJarFile = artifactResolver.getArtifact(groupId, artifactId, version); + if (deploymentJarFile != null) { + IPath deploymentJarFilePath = new Path(deploymentJarFile); + String deploymentJarName = deploymentJarFilePath.lastSegment(); + if (!existingJars.contains(deploymentJarName)) { + // The *-deployment JAR is not included in the classpath project, add it. + existingJars.add(deploymentJarName); + IPath sourceAttachmentPath = null; + // Get or download deployment sources JAR + String sourceJarFile = artifactResolver.getSources(groupId, artifactId, version); + if (sourceJarFile != null) { + sourceAttachmentPath = new Path(sourceJarFile); + } + deploymentJarEntries.add(JavaCore.newLibraryEntry(deploymentJarFilePath, + sourceAttachmentPath, null)); + } + } + } + } + } catch (Exception e) { + // do nothing + } + + break; + } + } + // Add the Quarkus project in classpath to resolve dependencies of deployment + // Quarkus JARs. + deploymentJarEntries.add(JavaCore.newProjectEntry(project.getProject().getLocation())); + } + + @Override + protected void processAnnotation(IJavaElement javaElement, IAnnotation annotation, String annotationName, + SearchContext context, IPropertiesCollector collector, IProgressMonitor monitor) throws JavaModelException { + Map javadocCache = (Map) context + .get(JAVADOC_CACHE_KEY); + processConfigRoot(javaElement, annotation, javadocCache, collector, monitor); + } + + // ------------- Process Quarkus ConfigRoot ------------- + + /** + * Process Quarkus ConfigRoot annotation from the given + * javaElement. + * + * @param javaElement the class, field element which have a Quarkus + * ConfigRoot annotations. + * @param configRootAnnotation the Quarkus ConfigRoot annotation. + * @param converter the documentation converter to use. + * @param javadocCache + * @param collector the properties to fill. + * @param monitor the progress monitor. + */ + private static void processConfigRoot(IJavaElement javaElement, IAnnotation configRootAnnotation, + Map javadocCache, IPropertiesCollector collector, + IProgressMonitor monitor) throws JavaModelException { + ConfigPhase configPhase = getConfigPhase(configRootAnnotation); + String configRootAnnotationName = getConfigRootName(configRootAnnotation); + String extension = getExtensionName(getSimpleName(javaElement), configRootAnnotationName, configPhase); + if (extension == null) { + return; + } + // Location (JAR, src) + IPackageFragmentRoot packageRoot = (IPackageFragmentRoot) javaElement + .getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT); + String location = packageRoot.getPath().toString(); + // Quarkus Extension name + String extensionName = JDTQuarkusUtils.getExtensionName(location); + + String baseKey = QUARKUS_PREFIX + extension; + processConfigGroup(location, extensionName, javaElement, baseKey, configPhase, javadocCache, collector, + monitor); + } + + /** + * Returns the Quarkus @ConfigRoot(phase=...) value. + * + * @param configRootAnnotation + * @return the Quarkus @ConfigRoot(phase=...) value. + * @throws JavaModelException + */ + private static ConfigPhase getConfigPhase(IAnnotation configRootAnnotation) throws JavaModelException { + String value = getAnnotationMemberValue(configRootAnnotation, "phase"); + if (value != null) { + if (value.endsWith(ConfigPhase.RUN_TIME.name())) { + return ConfigPhase.RUN_TIME; + } + if (value.endsWith(ConfigPhase.BUILD_AND_RUN_TIME_FIXED.name())) { + return ConfigPhase.BUILD_AND_RUN_TIME_FIXED; + } + } + return ConfigPhase.BUILD_TIME; + } + + /** + * Returns the Quarkus @ConfigRoot(name=...) value and + * {@link ConfigItem#HYPHENATED_ELEMENT_NAME} otherwise. + * + * @param configRootAnnotation @ConfigRoot annotation + * @return the Quarkus @ConfigRoot(name=...) value and + * {@link ConfigItem#HYPHENATED_ELEMENT_NAME} otherwise. + * @throws JavaModelException + */ + private static String getConfigRootName(IAnnotation configRootAnnotation) throws JavaModelException { + String value = getAnnotationMemberValue(configRootAnnotation, "name"); + if (value != null) { + return value; + } + return ConfigItem.HYPHENATED_ELEMENT_NAME; + } + + /** + * Returns the simple name of the given javaElement + * + * @param javaElement the Java class element + * @return the simple name of the given javaElement + */ + private static String getSimpleName(IJavaElement javaElement) { + String elementName = javaElement.getElementName(); + int index = elementName.lastIndexOf('.'); + return index != -1 ? elementName.substring(index + 1, elementName.length()) : elementName; + } + + /** + * Returns the Quarkus extension name according the + * configRootClassSimpleName, configRootAnnotationName + * and configPhase. + * + * @param configRootClassSimpleName the simple class name where ConfigRoot is + * declared. + * @param configRootAnnotationName the name declared in the ConfigRoot + * annotation. + * @param configPhase the config phase. + * @see https://github.com/quarkusio/quarkus/blob/master/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigDefinition.java#L173 + * (registerConfigRoot) + * @return the Quarkus extension name according the + * configRootClassSimpleName, + * configRootAnnotationName and configPhase. + */ + private static String getExtensionName(String configRootClassSimpleName, String configRootAnnotationName, + ConfigPhase configPhase) { + // See + // https://github.com/quarkusio/quarkus/blob/master/core/deployment/src/main/java/io/quarkus/deployment/configuration/ConfigDefinition.java#L173 + // registerConfigRoot + final String containingName; + if (configPhase == ConfigPhase.RUN_TIME) { + containingName = join(withoutSuffix(lowerCaseFirst(camelHumpsIterator(configRootClassSimpleName)), "Config", + "Configuration", "RunTimeConfig", "RunTimeConfiguration")); + } else { + containingName = join(withoutSuffix(lowerCaseFirst(camelHumpsIterator(configRootClassSimpleName)), "Config", + "Configuration", "BuildTimeConfig", "BuildTimeConfiguration")); + } + final String name = configRootAnnotationName; + final String rootName; + if (name.equals(ConfigItem.PARENT)) { + // throw reportError(configRoot, "Root cannot inherit parent name because it has + // no parent"); + return null; + } else if (name.equals(ConfigItem.ELEMENT_NAME)) { + rootName = containingName; + } else if (name.equals(ConfigItem.HYPHENATED_ELEMENT_NAME)) { + rootName = join("-", + withoutSuffix(lowerCase(camelHumpsIterator(configRootClassSimpleName)), "config", "configuration")); + } else { + rootName = name; + } + return rootName; + } + + // ------------- Process Quarkus ConfigGroup ------------- + + /** + * Process Quarkus ConfigGroup annotation from the given + * javaElement. + * + * @param extensionName the Quarkus extension name + * @param location the JAR/src location + * + * @param javaElement the class, field element which have a Quarkus + * ConfigGroup annotations. + * @param baseKey the base key + * @param configPhase the phase + * @param converter the documentation converter to use. + * @param javadocCache the Javadoc cache + * @param collector the properties to fill. + * @param monitor the progress monitor. + * @throws JavaModelException + */ + private static void processConfigGroup(final String location, String extensionName, IJavaElement javaElement, + String baseKey, ConfigPhase configPhase, Map javadocCache, + IPropertiesCollector collector, IProgressMonitor monitor) throws JavaModelException { + if (javaElement.getElementType() == IJavaElement.TYPE) { + IJavaElement[] elements = ((IType) javaElement).getChildren(); + for (IJavaElement child : elements) { + if (child.getElementType() == IJavaElement.FIELD) { + IField field = (IField) child; + final IAnnotation configItemAnnotation = getAnnotation((IAnnotatable) field, + CONFIG_ITEM_ANNOTATION); + String name = configItemAnnotation == null ? hyphenate(field.getElementName()) + : getAnnotationMemberValue(configItemAnnotation, "name"); + if (name == null) { + name = ConfigItem.HYPHENATED_ELEMENT_NAME; + } + String subKey; + if (name.equals(ConfigItem.PARENT)) { + subKey = baseKey; + } else if (name.equals(ConfigItem.ELEMENT_NAME)) { + subKey = baseKey + "." + field.getElementName(); + } else if (name.equals(ConfigItem.HYPHENATED_ELEMENT_NAME)) { + subKey = baseKey + "." + hyphenate(field.getElementName()); + } else { + subKey = baseKey + "." + name; + } + final String defaultValue = configItemAnnotation == null ? ConfigItem.NO_DEFAULT + : getAnnotationMemberValue(configItemAnnotation, "defaultValue"); + + String fieldTypeName = getResolvedTypeName(field); + IType fieldClass = findType(field.getJavaProject(), fieldTypeName); + final IAnnotation configGroupAnnotation = getAnnotation((IAnnotatable) fieldClass, + CONFIG_GROUP_ANNOTATION); + if (configGroupAnnotation != null) { + processConfigGroup(location, extensionName, fieldClass, subKey, configPhase, javadocCache, + collector, monitor); + } else { + addMetdataProperty(location, extensionName, field, fieldTypeName, fieldClass, subKey, + defaultValue, javadocCache, configPhase, collector, monitor); + } + } + } + } + } + + private static void addMetdataProperty(String location, String extensionName, IField field, String fieldTypeName, + IType fieldClass, String propertyName, String defaultValue, + Map javadocCache, ConfigPhase configPhase, IPropertiesCollector collector, + IProgressMonitor monitor) throws JavaModelException { + + // Class type + String type = getPropertyType(fieldClass, fieldTypeName); + + // Javadoc + String docs = getJavadoc(field, javadocCache, monitor); + + // field and class source + String source = getPropertySource(field); + + // Enumerations + List enumerations = getPropertyEnumerations(fieldClass); + + // Default value for primitive type + if ("boolean".equals(fieldTypeName)) { + collector.addMetadataProperty(MicroProfileProjectInfo.DEFAULT_REFERENCE_TYPE, propertyName, type, + ConfigItem.NO_DEFAULT.equals(defaultValue) ? "false" : defaultValue, docs, location, extensionName, + source, enumerations, configPhase); + } else if (isNumber(fieldTypeName)) { + collector.addMetadataProperty(MicroProfileProjectInfo.DEFAULT_REFERENCE_TYPE, propertyName, type, + ConfigItem.NO_DEFAULT.equals(defaultValue) ? "0" : defaultValue, docs, location, extensionName, + source, enumerations, configPhase); + } else if (isMap(fieldTypeName)) { + // FIXME: find better mean to check field is a Map + // this code works only if user uses Map as declaration and not if they declare + // HashMap for instance + String[] rawTypeParameters = getRawTypeParameters(fieldTypeName); + if ((rawTypeParameters[0].trim().equals("java.lang.String"))) { + // The key Map must be a String + processMap(field, propertyName, rawTypeParameters[1], docs, location, extensionName, source, + configPhase, javadocCache, collector, monitor); + } + } else if (isList(fieldTypeName)) { + collector.addMetadataProperty(MicroProfileProjectInfo.DEFAULT_REFERENCE_TYPE, propertyName, type, + defaultValue, docs, location, extensionName, source, enumerations, configPhase); + } else if (isOptional(fieldTypeName)) { + MetadataProperty item = collector.addMetadataProperty(MicroProfileProjectInfo.DEFAULT_REFERENCE_TYPE, + propertyName, type, defaultValue, docs, location, extensionName, source, enumerations, configPhase); + item.setRequired(false); + } else { + collector.addMetadataProperty(MicroProfileProjectInfo.DEFAULT_REFERENCE_TYPE, propertyName, type, + defaultValue, docs, location, extensionName, source, enumerations, configPhase); + } + } + + private static String[] getRawTypeParameters(String fieldTypeName) { + int start = fieldTypeName.indexOf("<") + 1; + int end = fieldTypeName.lastIndexOf(">"); + String keyValue = fieldTypeName.substring(start, end); + int index = keyValue.indexOf(','); + return new String[] { keyValue.substring(0, index), keyValue.substring(index + 1, keyValue.length()) }; + } + + private static void processMap(IField field, String baseKey, String mapValueClass, String docs, String location, + String extensionName, String source, ConfigPhase configPhase, + Map javadocCache, IPropertiesCollector collector, + IProgressMonitor monitor) throws JavaModelException { + final String subKey = baseKey + ".{*}"; + if ("java.util.Map".equals(mapValueClass)) { + // ignore, Map must be parameterized + } else if (isMap(mapValueClass)) { + String[] rawTypeParameters = getRawTypeParameters(mapValueClass); + processMap(field, subKey, rawTypeParameters[1], docs, location, extensionName, source, configPhase, + javadocCache, collector, monitor); + } else if (isOptional(mapValueClass)) { + // Optionals are not allowed as a map value type + } else { + IType type = findType(field.getJavaProject(), mapValueClass); + if (type == null || isPrimitiveType(mapValueClass)) { + // This case comes from when mapValueClass is: + // - Simple type, like java.lang.String + // - Type which cannot be found (bad classpath?) + addMetdataProperty(location, extensionName, field, mapValueClass, null, subKey, null, javadocCache, + configPhase, collector, monitor); + } else { + processConfigGroup(location, extensionName, type, subKey, configPhase, javadocCache, collector, + monitor); + } + } + } + + /** + * Returns the Javadoc from the given field. There are 3 strategies to extract + * the Javadoc: + * + *
    + *
  • try to extract Javadoc from the source (from '.java' source file or from + * JAR which is linked to source).
  • + *
  • try to get Javadoc from the attached Javadoc.
  • + *
  • get Javadoc from the Quarkus properties file stored in JAR META-INF/ + *
+ * + * @param field + * @param javadocCache + * @param monitor + * @return + * @throws JavaModelException + */ + private static String getJavadoc(IField field, Map javadocCache, + IProgressMonitor monitor) throws JavaModelException { + // TODO: get Javadoc from source anad attached doc by processing Javadoc tag as + // markdown + // Try to get javadoc from sources + /* + * String javadoc = findJavadocFromSource(field); if (javadoc != null) { return + * javadoc; } // Try to get attached javadoc javadoc = + * field.getAttachedJavadoc(monitor); if (javadoc != null) { return javadoc; } + */ + // Try to get the javadoc inside the META-INF/quarkus-javadoc.properties of the + // JAR + IPackageFragmentRoot packageRoot = (IPackageFragmentRoot) field.getAncestor(IJavaElement.PACKAGE_FRAGMENT_ROOT); + Properties properties = javadocCache.get(packageRoot); + if (properties == null) { + properties = new Properties(); + javadocCache.put(packageRoot, properties); + IJarEntryResource quarkusJavadocResource = findJavadocFromQuakusJavadocProperties(packageRoot); + if (quarkusJavadocResource != null) { + try { + properties.load(quarkusJavadocResource.getContents()); + } catch (Exception e) { + // TODO : log it + e.printStackTrace(); + } + } + } + if (properties.isEmpty()) { + return null; + } + // The META-INF/quarkus-javadoc.properties stores Javadoc without $ . Ex: + // io.quarkus.deployment.SslProcessor.SslConfig.native_=Enable native SSL + // support. + + String fieldKey = field.getDeclaringType().getFullyQualifiedName() + "." + field.getElementName(); + + // Here field key contains '$' + // Ex : io.quarkus.deployment.SslProcessor$SslConfig.native_ + // replace '$' with '.' + fieldKey = fieldKey.replace('$', '.'); + return properties.getProperty(fieldKey); + } + + private static String findJavadocFromSource(IField field) throws JavaModelException { + ISourceRange range = field.getJavadocRange(); + if (range != null) { + int start = range.getOffset() - field.getSourceRange().getOffset(); + return field.getSource().substring(start, range.getLength()); + } + return null; + } + + private static IJarEntryResource findJavadocFromQuakusJavadocProperties(IPackageFragmentRoot packageRoot) + throws JavaModelException { + return JDTQuarkusSearchUtils.findPropertiesResource(packageRoot, QUARKUS_JAVADOC_PROPERTIES); + } + +}