blob: 970ca8035084c4ebf60f1ec1309ef8af76132535 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2015 Takari, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* Anton Tanasenko - initial API and implementation
*******************************************************************************/
package org.eclipse.m2e.editor.xml.mojo;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.net.URI;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.collect.ImmutableSet;
import com.google.common.reflect.ClassPath;
import com.google.common.reflect.ClassPath.ClassInfo;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IProgressMonitor;
import org.codehaus.plexus.classworlds.realm.ClassRealm;
/**
* Mirrors logic implemented in default maven mojo configurator with regards to discovering how a PlexusConfiguration
* can be applied to an arbitrary object tree.
*
* @see org.codehaus.plexus.component.configurator.BasicComponentConfigurator
* @see org.codehaus.plexus.component.configurator.converters.lookup.DefaultConverterLookup
* @see org.codehaus.plexus.component.configurator.converters.composite.ObjectWithFieldsConverter
* @since 1.6
*/
public class PlexusConfigHelper {
private static final Logger log = LoggerFactory.getLogger(PlexusConfigHelper.class);
private Map<Class<?>, List<MojoParameter>> processedClasses;
public PlexusConfigHelper() {
processedClasses = new HashMap<>();
}
public List<MojoParameter> loadParameters(ClassRealm realm, Class<?> paramClass, IProgressMonitor monitor)
throws CoreException {
if(monitor.isCanceled()) {
return Collections.emptyList();
}
List<MojoParameter> parameters = processedClasses.get(paramClass);
if(parameters == null) {
parameters = new ArrayList<>();
processedClasses.put(paramClass, parameters);
log.debug("Loading properties of {}", paramClass.getName());
Map<String, Type> properties = getClassProperties(paramClass);
for(Map.Entry<String, Type> e : properties.entrySet()) {
if(!monitor.isCanceled()) {
addParameter(realm, paramClass, e.getValue(), e.getKey(), null, parameters, false, null, null, null, monitor);
}
}
}
return parameters;
}
public void addParameter(ClassRealm realm, Class<?> enclosingClass, Type paramType, String name, String alias,
List<MojoParameter> parameters, boolean required, String expression, String description, String defaultValue,
IProgressMonitor monitor) throws CoreException {
Class<?> paramClass = getRawType(paramType);
if(paramClass == null) {
return;
}
// inline
if(isInline(paramClass)) {
parameters.add(configure(new MojoParameter(name, getTypeDisplayName(paramType)), required, expression,
description, defaultValue));
if(alias != null) {
parameters.add(configure(new MojoParameter(alias, getTypeDisplayName(paramType)), required, expression,
description, defaultValue));
}
return;
}
// map
if(Map.class.isAssignableFrom(paramClass)) {
// we can't do anything with maps, unfortunately
parameters.add(configure(new MojoParameter(name, getTypeDisplayName(paramType)).map(), required, expression,
description, defaultValue));
if(alias != null) {
parameters.add(configure(new MojoParameter(alias, getTypeDisplayName(paramType)).map(), required, expression,
description, defaultValue));
}
return;
}
// properties
if(Properties.class.isAssignableFrom(paramClass)) {
MojoParameter nested = new MojoParameter("property", "property",
Arrays.asList(new MojoParameter("name", "String"), new MojoParameter("value", "String")));
parameters.add(configure(new MojoParameter(name, getTypeDisplayName(paramType), nested), required, expression,
description, defaultValue));
if(alias != null) {
parameters.add(configure(new MojoParameter(alias, getTypeDisplayName(paramType), nested), required, expression,
description, defaultValue));
}
}
// collection/array
Type itemType = getItemType(paramType);
if(itemType != null) {
List<MojoParameter> nested = getItemParameters(realm, enclosingClass, name, itemType, monitor);
parameters.add(configure(new MojoParameter(name, getTypeDisplayName(paramType), nested), required, expression,
description, defaultValue));
if(alias != null) {
nested = getItemParameters(realm, enclosingClass, alias, itemType, monitor);
parameters.add(configure(new MojoParameter(alias, getTypeDisplayName(paramType), nested), required, expression,
description, defaultValue));
}
return;
}
// pojo
// skip classes without no-arg constructors
try {
paramClass.getConstructor();
} catch(NoSuchMethodException ex) {
return;
}
List<MojoParameter> nested = loadParameters(realm, paramClass, monitor);
parameters.add(configure(new MojoParameter(name, getTypeDisplayName(paramType), nested), required, expression,
description, defaultValue));
if(alias != null) {
parameters.add(configure(new MojoParameter(alias, getTypeDisplayName(paramType), nested), required, expression,
description, defaultValue));
}
}
public List<MojoParameter> getItemParameters(ClassRealm realm, Class<?> enclosingClass, String name, Type paramType,
IProgressMonitor monitor) throws CoreException {
Class<?> paramClass = getRawType(paramType);
if(paramClass == null || isInline(paramClass)) {
MojoParameter container = new MojoParameter(toSingularName(name), getTypeDisplayName(paramType)).multiple();
return Collections.singletonList(container);
}
if(Map.class.isAssignableFrom(paramClass) || Properties.class.isAssignableFrom(paramClass)) {
MojoParameter container = new MojoParameter(toSingularName(name), getTypeDisplayName(paramType)).multiple().map();
return Collections.singletonList(container);
}
Type itemType = getItemType(paramType);
if(itemType != null) {
List<MojoParameter> nested = getItemParameters(realm, enclosingClass, name, itemType, monitor);
MojoParameter container = new MojoParameter(toSingularName(name), getTypeDisplayName(paramType), nested)
.multiple();
return Collections.singletonList(container);
}
List<Class<?>> parameterClasses = getCandidateClasses(realm, enclosingClass, paramClass);
List<MojoParameter> parameters = new ArrayList<>();
for(Class<?> clazz : parameterClasses) {
String paramName;
if(clazz.equals(paramClass)) {
paramName = toSingularName(name);
} else {
paramName = clazz.getSimpleName();
paramName = Character.toLowerCase(paramName.charAt(0)) + paramName.substring(1);
}
List<MojoParameter> nested = loadParameters(realm, paramClass, monitor);
MojoParameter container = new MojoParameter(paramName, getTypeDisplayName(clazz), nested).multiple();
parameters.add(container);
}
return parameters;
}
public static MojoParameter configure(MojoParameter p, boolean required, String expression, String description,
String defaultValue) {
p.setRequired(required);
p.setExpression(expression);
p.setDescription(description);
p.setDefaultValue(defaultValue);
return p;
}
public static Class<?> getRawType(Type type) {
if(type instanceof Class) {
return (Class<?>) type;
}
if(type instanceof ParameterizedType) {
return (Class<?>) ((ParameterizedType) type).getRawType();
}
return null;
}
public static Type getItemType(Type paramType) {
Class<?> paramClass = getRawType(paramType);
if(paramClass != null && paramClass.isArray()) {
return paramClass.getComponentType();
}
if(!Collection.class.isAssignableFrom(paramClass)) {
return null;
}
if(paramType instanceof ParameterizedType) {
ParameterizedType pt = (ParameterizedType) paramType;
Type[] args = pt.getActualTypeArguments();
if(args.length > 0) {
return args[0];
}
}
return null;
}
public Map<String, Type> getClassProperties(Class<?> clazz) {
Map<String, Type> props = new HashMap<>();
for(Method m : clazz.getMethods()) {
if((m.getModifiers() & Modifier.STATIC) != 0) {
continue;
}
String name = m.getName();
if((name.startsWith("add") || name.startsWith("set")) && m.getParameterTypes().length == 1) { //$NON-NLS-1$ //$NON-NLS-2$
String prop = name.substring(3);
if(!prop.isEmpty()) {
prop = Character.toLowerCase(prop.charAt(0)) + prop.substring(1);
if(!props.containsKey(prop)) {
props.put(prop, m.getGenericParameterTypes()[0]);
}
}
}
}
Class<?> pClazz = clazz;
while(pClazz != null && !pClazz.equals(Object.class)) {
for(Field f : pClazz.getDeclaredFields()) {
if((f.getModifiers() & (Modifier.STATIC | Modifier.FINAL)) != 0) {
continue;
}
String prop = f.getName();
if(!props.containsKey(prop)) {
props.put(prop, f.getGenericType());
}
}
pClazz = pClazz.getSuperclass();
}
return props;
}
public List<Class<?>> getCandidateClasses(ClassRealm realm, Class<?> enclosingClass, Class<?> paramClass) {
String name = enclosingClass.getName();
int dot = name.lastIndexOf('.');
if(dot > 0) {
String pkg = name.substring(0, dot);
List<Class<?>> candidateClasses = null;
ClassPath cp;
try {
cp = ClassPath.from(realm);
} catch(IOException e) {
log.error(e.getMessage());
return Collections.singletonList(enclosingClass);
}
for(ClassInfo ci : cp.getTopLevelClasses(pkg)) {
Class<?> clazz;
try {
clazz = realm.loadClass(ci.getName());
} catch(ClassNotFoundException e) {
log.error(e.getMessage(), e);
continue;
}
if((clazz.getModifiers() & (Modifier.ABSTRACT)) != 0) {
continue;
}
if(!paramClass.isAssignableFrom(clazz)) {
continue;
}
// skip classes without no-arg constructors
try {
clazz.getConstructor();
} catch(NoSuchMethodException ex) {
continue;
}
if(candidateClasses == null) {
candidateClasses = new ArrayList<>();
}
candidateClasses.add(clazz);
}
if(candidateClasses != null) {
return candidateClasses;
}
}
return Collections.singletonList(paramClass);
}
public static boolean isInline(Class<?> paramClass) {
return INLINE_TYPES.contains(paramClass.getName()) || paramClass.isEnum();
}
public static String getTypeDisplayName(Type type) {
Class<?> clazz = getRawType(type);
if(clazz == null) {
return type.toString();
}
if(clazz.isArray()) {
return getTypeDisplayName(clazz.getComponentType()) + "[]"; //$NON-NLS-1$
}
if(type instanceof ParameterizedType) {
ParameterizedType ptype = (ParameterizedType) type;
StringBuilder sb = new StringBuilder();
sb.append(getTypeDisplayName(clazz)).append("&lt;"); //$NON-NLS-1$
boolean first = true;
for(Type arg : ptype.getActualTypeArguments()) {
if(first)
first = false;
else
sb.append(", "); //$NON-NLS-1$
sb.append(getTypeDisplayName(arg));
}
return sb.append("&gt;").toString(); //$NON-NLS-1$
}
String name = clazz.getName();
int idx = name.lastIndexOf('.');
if(idx == -1) {
return name;
}
// remove common package names
String pkg = name.substring(0, idx);
if(pkg.equals("java.lang") || pkg.equals("java.util") || pkg.equals("java.io")) { //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$
return clazz.getSimpleName();
}
return name;
}
public String toSingularName(String name) {
if(name == null || name.trim().isEmpty()) {
return name;
}
if(name.endsWith("ies")) { //$NON-NLS-1$
return name.substring(0, name.length() - 3) + "y"; //$NON-NLS-1$
} else if(name.endsWith("ches")) { //$NON-NLS-1$ $NON-NLS-2$
return name.substring(0, name.length() - 2);
} else if(name.endsWith("xes")) { //$NON-NLS-1$
return name.substring(0, name.length() - 2);
} else if(name.endsWith("s") && (name.length() != 1)) { //$NON-NLS-1$
return name.substring(0, name.length() - 1);
}
return name;
}
private static final Set<String> INLINE_TYPES;
static {
// @formatter:off
INLINE_TYPES = ImmutableSet.<String>of(
byte.class.getName(),
Byte.class.getName(),
short.class.getName(),
Short.class.getName(),
int.class.getName(),
Integer.class.getName(),
long.class.getName(),
Long.class.getName(),
float.class.getName(),
Float.class.getName(),
double.class.getName(),
Double.class.getName(),
boolean.class.getName(),
Boolean.class.getName(),
char.class.getName(),
Character.class.getName(),
String.class.getName(),
StringBuilder.class.getName(),
StringBuffer.class.getName(),
File.class.getName(),
URI.class.getName(),
URL.class.getName(),
Date.class.getName(),
"org.codehaus.plexus.configuration.PlexusConfiguration"
);
// @formatter:on
}
}