/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.sling.caconfig.impl.metadata;
import java.lang.reflect.Method;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.lang3.StringUtils;
import org.apache.sling.caconfig.annotation.Configuration;
import org.apache.sling.caconfig.annotation.Property;
import org.apache.sling.caconfig.spi.metadata.ConfigurationMetadata;
import org.apache.sling.caconfig.spi.metadata.PropertyMetadata;
/**
* Helper methods for parsing metadata from configuration annotation classes.
*/
public final class AnnotationClassParser {
private static final Pattern METHOD_NAME_MAPPING = Pattern.compile("(\\$\\$)|(\\$)|(__)|(_)");
private AnnotationClassParser() {
// static methods only
}
/**
* Checks if the given class is suitable to be mapped with context-aware configuration.
* The given class has to be an annotation class, and the {@link Configuration} annotation has to be present.
* @param clazz Given class
* @return True if class is suitable for context-aware configuration
*/
public static boolean isContextAwareConfig(Class<?> clazz) {
return clazz.isAnnotation() && clazz.isAnnotationPresent(Configuration.class);
}
/**
* Get configuration name for given configuration annotation class.
* @param clazz Annotation class
* @return Configuration name
*/
public static String getConfigurationName(Class<?> clazz) {
Configuration configAnnotation = clazz.getAnnotation(Configuration.class);
if (configAnnotation == null) {
return null;
}
return getConfigurationName(clazz, configAnnotation);
}
/**
* Get configuration name for given configuration annotation class.
* @param clazz Annotation class
* @param configAnnotation Configuration metadata
* @return Configuration name
*/
private static String getConfigurationName(Class<?> clazz, Configuration configAnnotation) {
String configName = configAnnotation.name();
if (StringUtils.isBlank(configName)) {
configName = clazz.getName();
}
return configName;
}
/**
* Implements the method name mapping as defined in OSGi R6 Compendium specification,
* Chapter 112. Declarative Services Specification, Chapter 112.8.2.1. Component Property Mapping.
* @param methodName Method name
* @return Mapped property name
*/
public static String getPropertyName(String methodName) {
Matcher matcher = METHOD_NAME_MAPPING.matcher(methodName);
StringBuffer mappedName = new StringBuffer();
while (matcher.find()) {
String replacement = "";
if (matcher.group(1) != null) {
replacement = "\\$";
}
if (matcher.group(2) != null) {
replacement = "";
}
if (matcher.group(3) != null) {
replacement = "_";
}
if (matcher.group(4) != null) {
replacement = ".";
}
matcher.appendReplacement(mappedName, replacement);
}
matcher.appendTail(mappedName);
return mappedName.toString();
}
/**
* Build configuration metadata by parsing the given annotation interface class and it's configuration annotations.
* @param clazz Configuration annotation class
* @return Configuration metadata
*/
public static ConfigurationMetadata buildConfigurationMetadata(Class<?> clazz) {
Configuration configAnnotation = clazz.getAnnotation(Configuration.class);
if (configAnnotation == null) {
throw new IllegalArgumentException("Class has not @Configuration annotation: " + clazz.getName());
}
// configuration metadata and property metadata
String configName = getConfigurationName(clazz, configAnnotation);
ConfigurationMetadata configMetadata = new ConfigurationMetadata(configName,
buildConfigurationMetadata_PropertyMetadata(clazz),
configAnnotation.collection())
.label(emptyToNull(configAnnotation.label()))
.description(emptyToNull(configAnnotation.description()))
.properties(propsArrayToMap(configAnnotation.property()));
return configMetadata;
}
/**
* Build configuration metadata by parsing the given annotation interface class which is used for nested configurations.
* @param clazz Configuration annotation class
* @return Configuration metadata
*/
private static ConfigurationMetadata buildConfigurationMetadata_Nested(Class<?> clazz, String configName, boolean collection) {
return new ConfigurationMetadata(configName,
buildConfigurationMetadata_PropertyMetadata(clazz),
collection);
}
private static Collection<PropertyMetadata<?>> buildConfigurationMetadata_PropertyMetadata(Class<?> clazz) {
// sort properties by order number, or alternatively by label, name
SortedSet<PropertyMetadata<?>> propertyMetadataSet = new TreeSet<>(new Comparator<PropertyMetadata<?>>() {
@Override
public int compare(PropertyMetadata<?> o1, PropertyMetadata<?> o2) {
int compare = Integer.compare(o1.getOrder(), o2.getOrder());
if (compare == 0) {
String sort1 = StringUtils.defaultString(o1.getLabel(), o1.getName());
String sort2 = StringUtils.defaultString(o2.getLabel(), o2.getName());
compare = sort1.compareTo(sort2);
}
return compare;
}
});
Method[] propertyMethods = clazz.getDeclaredMethods();
for (Method propertyMethod : propertyMethods) {
PropertyMetadata<?> propertyMetadata = buildPropertyMetadata(propertyMethod, propertyMethod.getReturnType());
propertyMetadataSet.add(propertyMetadata);
}
return propertyMetadataSet;
}
@SuppressWarnings("unchecked")
private static <T> PropertyMetadata<T> buildPropertyMetadata(Method propertyMethod, Class<T> type) {
String propertyName = getPropertyName(propertyMethod.getName());
PropertyMetadata<?> propertyMetadata;
if (type.isArray() && type.getComponentType().isAnnotation()) {
ConfigurationMetadata nestedConfigMetadata = buildConfigurationMetadata_Nested(type.getComponentType(), propertyName, true);
propertyMetadata = new PropertyMetadata<>(propertyName, ConfigurationMetadata[].class)
.configurationMetadata(nestedConfigMetadata);
}
else if (type.isAnnotation()) {
ConfigurationMetadata nestedConfigMetadata = buildConfigurationMetadata_Nested(type, propertyName, false);
propertyMetadata = new PropertyMetadata<>(propertyName, ConfigurationMetadata.class)
.configurationMetadata(nestedConfigMetadata);
}
else {
propertyMetadata = new PropertyMetadata<>(propertyName, type)
.defaultValue((T)propertyMethod.getDefaultValue());
}
Property propertyAnnotation = propertyMethod.getAnnotation(Property.class);
if (propertyAnnotation != null) {
propertyMetadata.label(emptyToNull(propertyAnnotation.label()))
.description(emptyToNull(propertyAnnotation.description()))
.properties(propsArrayToMap(propertyAnnotation.property()))
.order(propertyAnnotation.order());
}
else {
Map<String,String> emptyMap = Collections.emptyMap();
propertyMetadata.properties(emptyMap);
}
return (PropertyMetadata)propertyMetadata;
}
private static String emptyToNull(String value) {
if (StringUtils.isEmpty(value)) {
return null;
}
else {
return value;
}
}
private static Map<String,String> propsArrayToMap(String[] properties) {
Map<String,String> props = new HashMap<>();
for (String property : properties) {
int index = StringUtils.indexOf(property, "=");
if (index >= 0) {
String key = property.substring(0, index);
String value = property.substring(index + 1);
props.put(key, value);
}
}
return props;
}
}