/*******************************************************************************
* Copyright (c) 2014-2016 Pivotal, Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Pivotal, Inc. - initial API and implementation
*******************************************************************************/
package org.springframework.ide.eclipse.boot.properties.editor.util;
import static org.springframework.ide.eclipse.boot.properties.editor.util.ArrayUtils.firstElement;
import static org.springframework.ide.eclipse.boot.properties.editor.util.ArrayUtils.lastElement;
import java.lang.reflect.Field;
import java.net.InetAddress;
import java.nio.charset.Charset;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.TreeSet;
import java.util.stream.Collectors;
import javax.inject.Provider;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jdt.core.Flags;
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.core.Signature;
import org.springframework.boot.configurationmetadata.Deprecation;
import org.springframework.ide.eclipse.boot.core.BootActivator;
import org.springframework.ide.eclipse.boot.properties.editor.metadata.DeprecationUtil;
import org.springframework.ide.eclipse.boot.properties.editor.metadata.StsValueHint;
import org.springframework.ide.eclipse.boot.properties.editor.metadata.ValueProviderRegistry.ValueProviderStrategy;
import org.springframework.ide.eclipse.boot.properties.editor.reconciling.AlwaysFailingParser;
import org.springframework.ide.eclipse.boot.util.Log;
import org.springframework.ide.eclipse.editor.support.util.CollectionUtil;
import org.springframework.ide.eclipse.editor.support.util.EnumValueParser;
import org.springframework.ide.eclipse.editor.support.util.HtmlSnippet;
import org.springframework.ide.eclipse.editor.support.util.StringUtil;
import org.springframework.ide.eclipse.editor.support.util.ValueParser;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableSet;
import com.google.common.net.MediaType;
import reactor.core.publisher.Flux;
/**
* Utilities to work with types represented as Strings as returned by
* Spring config metadata apis.
*
* @author Kris De Volder
*/
public class TypeUtil {
private static abstract class RadixableParser implements ValueParser {
protected abstract Object parse(String str, int radix);
@Override
public Object parse(String str) {
if (str.startsWith("0")) {
if (str.startsWith("0x")||str.startsWith("0X")) {
return parse(str.substring(2), 16);
} else if (str.startsWith("0b") || str.startsWith("0B")) {
return parse(str.substring(2), 2);
} else {
return parse(str, 8);
}
}
return parse(str, 10);
}
}
private static final Object OBJECT_TYPE_NAME = Object.class.getName();
private static final String STRING_TYPE_NAME = String.class.getName();
private static final String INET_ADDRESS_TYPE_NAME = InetAddress.class.getName();
private static final String CLASS_TYPE_NAME = Class.class.getName();
public enum BeanPropertyNameMode {
HYPHENATED(true,false), //bean property name in hyphenated form. E.g 'some-property-name'
CAMEL_CASE(false,true), //bean property name in camelCase. E.g. 'somePropertyName'
ALIASED(true,true); //use both as aliases of one another.
private final boolean includesHyphenated;
private final boolean includesCamelCase;
BeanPropertyNameMode(boolean hyphenated, boolean camelCase) {
this.includesCamelCase = camelCase;
this.includesHyphenated = hyphenated;
}
public boolean includesHyphenated() {
return includesHyphenated;
}
public boolean includesCamelCase() {
return includesCamelCase;
}
}
public enum EnumCaseMode {
LOWER_CASE, //convert enum names to lower case
ORIGNAL, //keep orignal enum name
ALIASED //use both lower-cased and original names as aliases of one another
}
private IJavaProject javaProject;
public TypeUtil(IJavaProject jp) {
//Note javaProject is allowed to be null, but only in unit testing context
// (This is so some tests can be run without an explicit jp needing to be created)
this.javaProject = jp;
}
private static final Map<String, String> PRIMITIVE_TYPE_NAMES = new HashMap<>();
private static final Map<String, Type> PRIMITIVE_TO_BOX_TYPE = new HashMap<>();
static {
PRIMITIVE_TYPE_NAMES.put("java.lang.Boolean", "boolean");
PRIMITIVE_TYPE_NAMES.put("java.lang.Byte", "byte");
PRIMITIVE_TYPE_NAMES.put("java.lang.Short", "short");
PRIMITIVE_TYPE_NAMES.put("java.lang.Integer","int");
PRIMITIVE_TYPE_NAMES.put("java.lang.Long", "long");
PRIMITIVE_TYPE_NAMES.put("java.lang.Double", "double");
PRIMITIVE_TYPE_NAMES.put("java.lang.Float", "float");
PRIMITIVE_TYPE_NAMES.put("java.lang.Character", "char");
for (Entry<String, String> e : PRIMITIVE_TYPE_NAMES.entrySet()) {
PRIMITIVE_TO_BOX_TYPE.put(e.getValue(), new Type(e.getKey(), null));
}
}
public static final Type INTEGER_TYPE = new Type("java.lang.Integer", null);
private static final Set<String> ASSIGNABLE_TYPES = new HashSet<>(Arrays.asList(
"java.lang.Boolean",
"java.lang.String",
"java.lang.Short",
"java.lang.Integer",
"java.lang.Long",
"java.lang.Double",
"java.lang.Float",
"java.lang.Character",
"java.lang.Byte",
INET_ADDRESS_TYPE_NAME,
CLASS_TYPE_NAME,
"java.lang.String[]"
));
private static final Set<String> ATOMIC_TYPES = new HashSet<>(PRIMITIVE_TYPE_NAMES.keySet());
static {
ATOMIC_TYPES.add(INET_ADDRESS_TYPE_NAME);
ATOMIC_TYPES.add(STRING_TYPE_NAME);
ATOMIC_TYPES.add(CLASS_TYPE_NAME);
}
private static final Map<String, String[]> TYPE_VALUES = new HashMap<>();
static {
TYPE_VALUES.put("java.lang.Boolean", new String[] { "true", "false" });
}
private static final Map<String,ValueParser> VALUE_PARSERS = new HashMap<>();
static {
VALUE_PARSERS.put(Byte.class.getName(), new RadixableParser() {
public Object parse(String str, int radix) {
return Byte.parseByte(str, radix);
}
});
VALUE_PARSERS.put(Integer.class.getName(), new RadixableParser() {
public Object parse(String str, int radix) {
return Integer.parseInt(str, radix);
}
});
VALUE_PARSERS.put(Long.class.getName(), new RadixableParser() {
public Object parse(String str, int radix) {
return Long.parseLong(str, radix);
}
});
VALUE_PARSERS.put(Short.class.getName(), new RadixableParser() {
public Object parse(String str, int radix) {
return Short.parseShort(str, radix);
}
});
VALUE_PARSERS.put(Double.class.getName(), new ValueParser() {
public Object parse(String str) {
return Double.parseDouble(str);
}
});
VALUE_PARSERS.put(Float.class.getName(), new ValueParser() {
public Object parse(String str) {
return Float.parseFloat(str);
}
});
VALUE_PARSERS.put(Boolean.class.getName(), new ValueParser() {
public Object parse(String str) {
//The 'more obvious' implementation is too liberal and accepts anything as okay.
//return Boolean.parseBoolean(str);
str = str.toLowerCase();
if (str.equals("true")) {
return true;
} else if (str.equals("false")) {
return false;
}
throw new IllegalArgumentException("Value should be 'true' or 'false'");
}
});
}
public ValueParser getValueParser(Type type) {
ValueParser simpleParser = VALUE_PARSERS.get(type.getErasure());
if (simpleParser!=null) {
return simpleParser;
}
Collection<StsValueHint> enumValues = getAllowedValues(type, EnumCaseMode.ALIASED);
if (enumValues!=null) {
//Note, technically if 'enumValues is empty array' this means something different
// from when it is null. An empty array means a type that has no values, so
// assigning anything to it is an error.
return new EnumValueParser(niceTypeName(type), getBareValues(enumValues));
}
if (isMap(type)) {
//Trying to parse map types from scalars is not possible. Thus we
// provide a parser that allows throws
return new AlwaysFailingParser(niceTypeName(type));
}
return null;
}
private String[] getBareValues(Collection<StsValueHint> hints) {
if (hints!=null) {
String[] values = new String[hints.size()];
int i = 0;
for (StsValueHint h : hints) {
values[i++] = h.getValue();
}
return values;
}
return null;
}
/**
* @return An array of allowed values for a given type. If an array is returned then
* *only* values in the array are valid and using any other value constitutes an error.
* This may return null if allowedValues list is unknown or the type is not characterizable
* as a simple enumaration of allowed values.
* @param caseMode determines whether Enum values are returned in 'lower case form', 'orignal form',
* or 'aliased' (meaning both forms are returned).
*/
public Collection<StsValueHint> getAllowedValues(Type enumType, EnumCaseMode caseMode) {
if (enumType!=null) {
try {
String[] values = TYPE_VALUES.get(enumType.getErasure());
if (values!=null) {
if (caseMode==EnumCaseMode.ALIASED) {
ImmutableSet.Builder<String> aliased = ImmutableSet.builder();
aliased.add(values);
for (int i = 0; i < values.length; i++) {
aliased.add(values[i].toUpperCase());
}
return aliased.build().stream().map(StsValueHint::create).collect(Collectors.toList());
} else {
return Arrays.stream(values).map(StsValueHint::create).collect(Collectors.toList());
}
}
IType type = findType(enumType.getErasure());
if (type!=null && type.isEnum()) {
IField[] fields = type.getFields();
if (fields!=null) {
ImmutableList.Builder<StsValueHint> enums = ImmutableList.builder();
boolean addOriginal = caseMode==EnumCaseMode.ORIGNAL||caseMode==EnumCaseMode.ALIASED;
boolean addLowerCased = caseMode==EnumCaseMode.LOWER_CASE||caseMode==EnumCaseMode.ALIASED;
for (int i = 0; i < fields.length; i++) {
IField f = fields[i];
Provider<HtmlSnippet> jdoc = StsValueHint.javaDocSnippet(f);
if (f.isEnumConstant()) {
String rawName = f.getElementName();
if (addOriginal) {
enums.add(StsValueHint.create(rawName, f));
}
if (addLowerCased) {
enums.add(StsValueHint.create(StringUtil.upperCaseToHyphens(rawName), f));
}
}
}
return enums.build();
}
}
} catch (Exception e) {
BootActivator.log(e);
}
}
return null;
}
public String niceTypeName(Type _type) {
StringBuilder buf = new StringBuilder();
niceTypeName(_type, buf);
return buf.toString();
}
public void niceTypeName(Type type, StringBuilder buf) {
if (type==null) {
buf.append("null");
return;
}
String typeStr = type.getErasure();
String primTypeName = PRIMITIVE_TYPE_NAMES.get(typeStr);
if (primTypeName!=null) {
buf.append(primTypeName);
} else if (typeStr.startsWith("java.lang.")) {
buf.append(typeStr.substring("java.lang.".length()));
} else if (typeStr.startsWith("java.util.")) {
buf.append(typeStr.substring("java.util.".length()));
} else {
buf.append(typeStr);
}
if (isEnum(type)) {
Collection<StsValueHint> values = getAllowedValues(type, EnumCaseMode.ORIGNAL);
if (values!=null && !values.isEmpty()) {
buf.append("[");
int i = 0;
for (StsValueHint hint : values) {
if (i>0) {
buf.append(", ");
}
buf.append(hint.getValue());
i++;
if (i>=4) {
break;
}
}
if (i<values.size()) {
buf.append(", ...");
}
buf.append("]");
}
} else if (type.isGeneric()) {
Type[] params = type.getParams();
buf.append("<");
for (int i = 0; i < params.length; i++) {
if (i>0) {
buf.append(", ");
}
niceTypeName(params[i], buf);
}
buf.append(">");
}
}
/**
* @return true if it is reasonable to navigate given type with '.' notation. This returns true
* by default except for some specific cases we assume are not 'dotable' such as Primitive types
* and String
*/
public boolean isDotable(Type type) {
String typeName = type.getErasure();
if (typeName.equals("java.lang.Object")) {
//special case. Treat as 'non dotable' type. This mainly for stuff like logging.level
// declared as Map<String,Object> so it would 'eat' the dots into the key.
// also it makes sense to treat Object as 'non-dotable' since we cannot determine properties
// for such an abstract type (as Object itself has no setters).
return false;
}
return !isAtomic(type);
}
public static boolean isObject(Type type) {
return type!=null && OBJECT_TYPE_NAME.equals(type.getErasure());
}
public static boolean isString(Type type) {
return type!=null && STRING_TYPE_NAME.equals(type.getErasure());
}
public boolean isAtomic(Type type) {
if (type!=null) {
String typeName = type.getErasure();
return ATOMIC_TYPES.contains(typeName) || isEnum(type);
}
return false;
}
/**
* Check if it is valid to
* use the notation <name>[<index>]=<value> in property file
* for properties of this type.
*/
public static boolean isBracketable(Type type) {
//Note array types where once not considered 'Bracketable'
//see: STS-4031
//However...
//Seems that in Boot 1.3 arrays are now 'Bracketable' and funcion much equivalnt to list (even including 'autogrowing' them).
//This is actually more logical too.
//So '[' notation in props file can be used for either list or arrays (at leats in recent versions of boot).
return isArray(type) || isList(type);
}
public static boolean isList(Type type) {
//Note: to be really correct we should use JDT infrastructure to resolve
//type in project classpath instead of using Java reflection.
//However, use reflection here is okay assuming types we care about
//are part of JRE standard libraries. Using eclipse 'type hirearchy' would
//also potentialy be very slow.
if (type!=null) {
String erasure = type.getErasure();
try {
Class<?> erasureClass = Class.forName(erasure);
return List.class.isAssignableFrom(erasureClass);
} catch (Exception e) {
//type not resolveable assume its not 'array like'
}
}
return false;
}
/**
* Check if type can be treated / represented as a sequence node in .yml file
*/
public static boolean isSequencable(Type type) {
return isList(type) || isArray(type);
}
public static boolean isArray(Type type) {
return type!=null && type.getErasure().endsWith("[]");
}
public static boolean isMap(Type type) {
//Note: to be really correct we should use JDT infrastructure to resolve
//type in project classpath instead of using Java reflection.
//However, use reflection here is okay assuming types we care about
//are part of JRE standard libraries. Using eclipse 'type hirearchy' would
//also potentialy be very slow.
if (type!=null) {
String erasure = type.getErasure();
try {
Class<?> erasureClass = Class.forName(erasure);
return Map.class.isAssignableFrom(erasureClass);
} catch (Exception e) {
//type not resolveable
}
}
return false;
}
/**
* Get domain type for a map or list generic type.
*/
public static Type getDomainType(Type type) {
if (isArray(type)) {
return getArrayDomainType(type);
} else {
return lastElement(type.getParams());
}
}
private static Type getArrayDomainType(Type type) {
if (type!=null) {
String fullName = type.getErasure();
Assert.isLegal(fullName.endsWith("[]"));
String elName = fullName.substring(0, fullName.length()-2);
return normalizePrimitiveType(new Type(elName, null));
}
return null;
}
/**
* Convert a type which is a 'primitive' type like 'int', 'long' etc. to its
* corresponding 'Boxed' type. If the type isn't a primitive type then
* just return it unchanged.
*/
private static Type normalizePrimitiveType(Type type) {
if (type!=null) {
String name = type.getErasure();
Type boxType = PRIMITIVE_TO_BOX_TYPE.get(name);
if (boxType!=null) {
return boxType;
}
}
return type;
}
public Type getKeyType(Type mapOrArrayType) {
if (isSequencable(mapOrArrayType)) {
return INTEGER_TYPE;
} else {
//assumed to be a map
return firstElement(mapOrArrayType.getParams());
}
}
public boolean isAssignableType(Type type) {
return ASSIGNABLE_TYPES.contains(type.getErasure())
|| isEnum(type)
|| isAssignableList(type);
}
private boolean isAssignableList(Type type) {
//TODO: isBracketable means 'isList' right now, but this may not be
// the case in the future.
if (isBracketable(type)) {
Type domainType = getDomainType(type);
return isAtomic(domainType);
}
return false;
}
public boolean isEnum(Type type) {
try {
IType eclipseType = findType(type.getErasure());
if (eclipseType!=null) {
return eclipseType.isEnum();
}
} catch (Exception e) {
Log.log(e);
}
return false;
}
private IType findType(String typeName) {
try {
if (javaProject!=null) {
return javaProject.findType(typeName);
}
} catch (Exception e) {
Log.log(e);
}
return null;
}
private IType findType(Type beanType) {
return findType(beanType.getErasure());
}
private static final String[] NO_PARAMS = new String[0];
private static final Map<String, ValueProviderStrategy> VALUE_HINTERS = new HashMap<>();
static {
valueHints("java.nio.charset.Charset", new LazyProvider<String[]>() {
@Override
protected String[] compute() {
Set<String> charsets = Charset.availableCharsets().keySet();
return charsets.toArray(new String[charsets.size()]);
}
});
valueHints("java.util.Locale", new LazyProvider<String[]>() {
@Override
protected String[] compute() {
Locale[] locales = SimpleDateFormat.getAvailableLocales();
String[] names = new String[locales.length];
for (int i = 0; i < names.length; i++) {
names[i] = locales[i].toString();
}
return names;
}
});
valueHints("org.springframework.util.MimeType", new LazyProvider<String[]>() {
@Override
protected String[] compute() {
try {
Field f = MediaType.class.getDeclaredField("KNOWN_TYPES");
f.setAccessible(true);
@SuppressWarnings("unchecked")
Map<MediaType, MediaType> map = (Map<MediaType, MediaType>) f.get(null);
TreeSet<String> mediaTypes = new TreeSet<>();
for (MediaType m : map.keySet()) {
mediaTypes.add(m.toString());
}
return mediaTypes.toArray(new String[mediaTypes.size()]);
} catch (Exception e) {
BootActivator.log(e);
}
return null;
}
});
valueHints("org.springframework.core.io.Resource", new ResourceHintProvider());
}
/**
* Determine properties that are setable on object of given type.
* <p>
* Note that this may return both null or an empty list, but they mean
* different things. Null means that the properties on the object are not known,
* and therefore reconciling should not check property validity. On the other hand
* returning an empty list means that there are no properties. In this case,
* accessing properties is invalid and reconciler should show an error message
* for any property access.
*
* @return A list of known properties or null if the list of properties is unknown.
*/
public List<TypedProperty> getProperties(Type type, EnumCaseMode enumMode, BeanPropertyNameMode beanMode) {
if (type==null) {
return null;
}
if (!isDotable(type)) {
//If dot navigation is not valid then really this is just like saying the type has no properties.
return Collections.emptyList();
}
if (isMap(type)) {
Type keyType = getKeyType(type);
if (keyType!=null) {
Collection<StsValueHint> keyHints = getAllowedValues(keyType, enumMode);
if (CollectionUtil.hasElements(keyHints)) {
Type valueType = getDomainType(type);
ArrayList<TypedProperty> properties = new ArrayList<>(keyHints.size());
for (StsValueHint hint : keyHints) {
String propName = hint.getValue();
properties.add(new TypedProperty(propName, valueType, hint.getDescriptionProvider(), hint.getDeprecation()));
}
return properties;
}
}
} else {
String typename = type.getErasure();
IType eclipseType = findType(typename);
//TODO: handle type parameters.
if (eclipseType!=null) {
List<IMethod> getters = getGetterMethods(eclipseType);
//TODO: getters inherited from super classes?
if (getters!=null && !getters.isEmpty()) {
ArrayList<TypedProperty> properties = new ArrayList<>(getters.size());
for (IMethod m : getters) {
Deprecation deprecation = DeprecationUtil.extract(m);
Type propType = null;
try {
propType = Type.fromSignature(m.getReturnType(), eclipseType);
} catch (JavaModelException e) {
Log.log(e);
}
if (beanMode.includesHyphenated()) {
properties.add(new TypedProperty(getterOrSetterNameToProperty(m.getElementName()), propType, deprecation));
}
if (beanMode.includesCamelCase()) {
properties.add(new TypedProperty(getterOrSetterNameToCamelName(m.getElementName()), propType, deprecation));
}
}
return properties;
}
}
}
return null;
}
/**
* Registers a strategy for providing value hints with a given typeName.
*/
public static void valueHints(String typeName, ValueProviderStrategy provider) {
Assert.isLegal(!VALUE_HINTERS.containsKey(typeName)); //Only one value hinter per type is supported at the moment
ATOMIC_TYPES.add(typeName); //valueHints typically implies that the type should be treated as atomic as well.
ASSIGNABLE_TYPES.add(typeName); //valueHints typically implies that the type should be treated as atomic as well.
VALUE_HINTERS.put(typeName, provider);
}
/**
* Registers a strategy for providing value hints with a given typeName.
*/
public static void valueHints(String typeName, Provider<String[]> provider) {
valueHints(typeName, new ValueProviderStrategy() {
@Override
public Flux<StsValueHint> getValues(IJavaProject javaProject, String query) {
String[] values = provider.get();
if (ArrayUtils.hasElements(values)) {
return Flux.fromArray(values)
.map(StsValueHint::create);
}
return Flux.empty();
}
});
}
private String getterOrSetterNameToProperty(String name) {
String camelName = getterOrSetterNameToCamelName(name);
return StringUtil.camelCaseToHyphens(camelName);
}
public String getterOrSetterNameToCamelName(String name) {
Assert.isLegal(name.startsWith("set") || name.startsWith("get") || name.startsWith("is"));
int prefixLen = name.startsWith("is") ? 2 : 3;
String camelName = Character.toLowerCase(name.charAt(prefixLen)) + name.substring(prefixLen+1);
return camelName;
}
private List<IMethod> getGetterMethods(IType eclipseType) {
try {
if (eclipseType!=null && eclipseType.isClass()) {
IMethod[] allMethods = eclipseType.getMethods();
if (ArrayUtils.hasElements(allMethods)) {
ArrayList<IMethod> getters = new ArrayList<>();
for (IMethod m : allMethods) {
if (!isStatic(m) && isPublic(m)) {
String mname = m.getElementName();
if (
(mname.startsWith("get") && mname.length()>=4) ||
(mname.startsWith("is") && mname.length()>=3)
) {
//Need at least 4 chars or the property name will be empty.
String sig = m.getSignature();
int numParams = Signature.getParameterCount(sig);
if (numParams==0) {
getters.add(m);
}
}
}
}
return getters;
}
}
} catch (Exception e) {
BootActivator.log(e);
}
return null;
}
// private List<IMethod> getSetterMethods(IType eclipseType) {
// try {
// if (eclipseType!=null && eclipseType.isClass()) {
// IMethod[] allMethods = eclipseType.getMethods();
// if (ArrayUtils.hasElements(allMethods)) {
// ArrayList<IMethod> setters = new ArrayList<IMethod>();
// for (IMethod m : allMethods) {
// String mname = m.getElementName();
// if (mname.startsWith("set") && mname.length()>=4) {
// //Need at least 4 chars or the property name will be empty.
// String sig = m.getSignature();
// int numParams = Signature.getParameterCount(sig);
// if (numParams==1) {
// setters.add(m);
// }
// }
// }
// return setters;
// }
// }
// } catch (Exception e) {
// BootActivator.log(e);
// }
// return null;
// }
private boolean isStatic(IMethod m) {
try {
return Flags.isStatic(m.getFlags());
} catch (JavaModelException e) {
//Couldn't determine if it was public or not... let's assume it was NOT
// (will result in potentially more CA completions)
BootActivator.log(e);
return false;
}
}
private boolean isPublic(IMethod m) {
try {
return m.getDeclaringType().isInterface()
|| Flags.isPublic(m.getFlags());
} catch (JavaModelException e) {
//Couldn't determine if it was public or not... let's assume it WAS
// (will result in potentially more CA completions)
BootActivator.log(e);
return true;
}
}
public Map<String, TypedProperty> getPropertiesMap(Type type, EnumCaseMode enumMode, BeanPropertyNameMode beanMode) {
//TODO: optimize, produce directly as a map instead of
// first creating list and then coverting it.
List<TypedProperty> list = getProperties(type, enumMode, beanMode);
if (list!=null) {
Map<String, TypedProperty> map = new HashMap<>();
for (TypedProperty p : list) {
map.put(p.getName(), p);
}
return map;
}
return null;
}
/**
* Maybe ne null in some contexts. In such context functionality will be limited because
* types can not be resolved.
*/
public IJavaProject getJavaProject() {
return javaProject;
}
public IField getField(Type beanType, String propName) {
IType type = findType(beanType);
return getExactField(type, propName);
}
protected IField getExactField(IType type, String fieldName) {
IField f = type.getField(StringUtil.hyphensToCamelCase(fieldName, false));
if (f!=null && f.exists()) {
return f;
}
return null;
}
public IField getEnumConstant(Type enumType, String propName) {
IType type = findType(enumType);
//1: if propname is already spelled exactly...
IField f = getExactField(type, propName);
if (f!=null) return f;
//2: most likely enum constant is upper-case form of propname
String fieldName = StringUtil.hyphensToUpperCase(propName);
return getExactField(type, fieldName);
}
public IMethod getSetter(Type beanType, String propName) {
try {
String setterName = "set" + StringUtil.hyphensToCamelCase(propName, true);
IType type = findType(beanType);
for (IMethod m : type.getMethods()) {
if (setterName.equals(m.getElementName())) {
return m;
}
}
} catch (Exception e) {
BootActivator.log(e);
}
return null;
}
public IJavaElement getGetter(Type beanType, String propName) {
String getterName = "get" + StringUtil.hyphensToCamelCase(propName, true);
IType type = findType(beanType);
IMethod m = type.getMethod(getterName, NO_PARAMS);
if (m.exists()) {
return m;
}
return null;
}
public static String deprecatedPropertyMessage(String name, String contextType, String replace, String reason) {
StringBuilder msg = new StringBuilder("Property '"+name+"'");
if (StringUtil.hasText(contextType)) {
msg.append(" of type '"+contextType+"'");
}
boolean hasReplace = StringUtil.hasText(replace);
boolean hasReason = StringUtil.hasText(reason);
if (!hasReplace && !hasReason) {
msg.append(" is Deprecated!");
} else {
msg.append(" is Deprecated: ");
if (hasReplace) {
msg.append("Use '"+ replace +"' instead.");
if (hasReason) {
msg.append(" Reason: ");
}
}
if (hasReason) {
msg.append(reason);
}
}
return msg.toString();
}
public Collection<StsValueHint> getHintValues(Type type, String query, EnumCaseMode enumCaseMode) {
if (type!=null) {
Collection<StsValueHint> allowed = getAllowedValues(type, enumCaseMode);
if (allowed!=null) {
return allowed;
}
ValueProviderStrategy valueHinter = VALUE_HINTERS.get(type.getErasure());
if (valueHinter!=null) {
return valueHinter.getValuesNow(javaProject, query);
}
}
return null;
}
/**
* Determine the dimensionality of a collection-like type (i.e. a Map or List). The dimensionality
* is essentialy how many succesive 'indexing' operations need to be applied before reasing the actual elements.
* <p>
* For examle:
* List<String> -> 1
* List<List<String>> -> 2
* List<List<List<String>>> -> 2
* Map<*,List<String>> -> 2
*/
public static int getDimensionality(Type type) {
int dim = 0;
while (isSequencable(type) || isMap(type)) {
dim++;
type = getDomainType(type);
}
return dim;
}
public static boolean isClass(Type type) {
if (type!=null) {
return CLASS_TYPE_NAME.equals(type.getErasure());
}
return false;
}
///////////////////////////////////////////////////////////////////////////////////////////////////////
// Addapting our interface so it is compatible with YTypeUtil
//
// This allows our types to be used by the more generic stuff from the 'editor.support' plugin.
//
// Note, it may be possible to avoid having these 'adaptor' methods by making YTypeUtil a paramerized
// type. I.e something like "interface YTypeUtil<T extends YType>.
// Paramterizations like that tend to propagate fire and wide in the code and make for complicated
// signatures. For now using these bredging methods is simpler if perhaps a bit more error prone.
// @Override
// public boolean isAtomic(YType type) {
// return isAtomic((Type)type);
// }
//
// @Override
// public boolean isMap(YType type) {
// return isMap((Type)type);
// }
//
// @Override
// public boolean isSequencable(YType type) {
// return isSequencable((Type)type);
// }
//
// @Override
// public YType getDomainType(YType type) {
// return getDomainType((Type)type);
// }
//
// @Override
// public String[] getHintValues(YType type) {
// return getAllowedValues((Type) type, EnumCaseMode.ALIASED);
// }
//
// @SuppressWarnings("unchecked")
// @Override
// public List<YTypedProperty> getProperties(YType type) {
// //Dirty hack, passing this through a raw type to bypass the java type system
// //complaining the List<TypedProperty> is not compatible with List<YTypedProperty>
// //This dirty and 'illegal' conversion is okay because the list is only used for reading.
// @SuppressWarnings("rawtypes")
// List props = getProperties((Type)type, EnumCaseMode.ALIASED, BeanPropertyNameMode.ALIASED);
// return Collections.unmodifiableList(props);
// }
//
// @SuppressWarnings("unchecked")
// @Override
// public Map<String, YTypedProperty> getPropertiesMap(YType type) {
// //Dirty hack, see comment in getProperties(YType)
// @SuppressWarnings("rawtypes")
// Map map = getPropertiesMap((Type)type, EnumCaseMode.ALIASED, BeanPropertyNameMode.ALIASED);
// return Collections.unmodifiableMap(map);
// }
//
// @Override
// public String niceTypeName(YType type) {
// return niceTypeName((Type)type);
// }
//
// @Override
// public YType getKeyType(YType type) {
// return getKeyType((Type)type);
// }
}