Java tutorial
// ============================================================================ // // Copyright (C) 2006-2015 Talend Inc. - www.talend.com // // This source code is available under agreement available at // %InstallDIR%\features\org.talend.rcp.branding.%PRODUCTNAME%\%PRODUCTNAME%license.txt // // You should have received a copy of the agreement // along with this program; if not, write to Talend SA // 9 rue Pages 92150 Suresnes, France // // ============================================================================ package org.talend.daikon.properties; import java.lang.reflect.Constructor; import java.lang.reflect.Field; import java.lang.reflect.InvocationTargetException; import java.util.*; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.builder.EqualsBuilder; import org.apache.commons.lang3.builder.HashCodeBuilder; import org.talend.daikon.NamedThing; import org.talend.daikon.exception.ExceptionContext; import org.talend.daikon.exception.TalendRuntimeException; import org.talend.daikon.exception.error.CommonErrorCodes; import org.talend.daikon.i18n.TranslatableImpl; import org.talend.daikon.properties.error.PropertiesErrorCode; import org.talend.daikon.properties.presentation.Form; import org.talend.daikon.properties.property.Property; import org.talend.daikon.properties.property.Property.Flags; import org.talend.daikon.properties.property.PropertyValueEvaluator; import org.talend.daikon.properties.property.PropertyVisitor; import org.talend.daikon.serialize.PostDeserializeHandler; import org.talend.daikon.serialize.PostDeserializeSetup; import org.talend.daikon.strings.ToStringIndent; import org.talend.daikon.strings.ToStringIndentUtil; import com.cedarsoftware.util.io.JsonWriter; /** * Implementation of {@link Properties} which must be subclassed to define your properties. */ public class PropertiesImpl extends TranslatableImpl implements Properties, AnyProperty, PostDeserializeHandler, ToStringIndent { private static final long serialVersionUID = -7970336622844281900L; private String name; private transient List<Form> forms = new ArrayList<>(); ValidationResult validationResult; transient private boolean layoutAlreadyInitalized; transient private boolean propsAlreadyInitialized; /** * Handle post deserialization. * * If you need to do additional things to react to a specific version, you can subclass this (and call the * superclass <strong>last</strong>). */ @Override public boolean postDeserialize(int version, PostDeserializeSetup setup, boolean persistent) { if (persistent) { // only handle local properties List<NamedThing> properties = getProperties(); for (NamedThing nt : properties) { if (nt instanceof Property) { Property<?> property = (Property<?>) nt; if (property.isFlag(Flags.ENCRYPT)) { property.encryptStoredValue(!ENCRYPT); } // else not an encrypted property } // else not a Property so ignors it. } } if (setup != null) { setup.setup(this); } if (persistent) { initLayout(); } List<NamedThing> properties = getProperties(); for (NamedThing prop : properties) { if (prop instanceof Property) { prop.setI18nMessageFormatter(getI18nMessageFormatter()); } } return false; } /** * named constructor to be used is these properties are nested in other properties. Do not subclass this method for * initialization, use {@link #init()} instead. * * @param name, uniquely identify the property among other properties when used as nested properties. */ public PropertiesImpl(String name) { setName(name); } @Override public Properties init() { // init nested properties starting from the bottom ones initProperties(); initLayout(); return this; } @Override public Properties initForRuntime() { initProperties(); return this; } private void initProperties() { if (!propsAlreadyInitialized) { List<Field> uninitializedProperties = initializeFields(); setupProperties(); // initialize all the properties that where found and not initialized // they must be initalized after the setup. for (Field f : uninitializedProperties) { NamedThing se; try { f.setAccessible(true); se = (NamedThing) f.get(this); if (se != null) { initializeField(f, se); } else {// field not initilaized but is should be (except for returns field) if (!acceptUninitializedField(f)) { throw new TalendRuntimeException(PropertiesErrorCode.PROPERTIES_HAS_UNITIALIZED_PROPS, ExceptionContext.withBuilder().put("name", this.getClass().getCanonicalName()) .put("field", f.getName()).build()); } // else a returns field that may not be initialized } } catch (IllegalArgumentException | IllegalAccessException e) { throw new TalendRuntimeException(CommonErrorCodes.UNEXPECTED_EXCEPTION, e); } } propsAlreadyInitialized = true; } // else already intialized } protected List<Field> initializeFields() { List<Field> uninitializedProperties = new ArrayList<>(); Field[] fields = getClass().getFields(); for (Field f : fields) { try { if (isAPropertyType(f.getType())) { f.setAccessible(true); NamedThing se = (NamedThing) f.get(this); if (se != null) { initializeField(f, se); } else {// not yet initialized to record it uninitializedProperties.add(f); } } // else not a field that ought to be initialized } catch (IllegalAccessException e) { throw new TalendRuntimeException(CommonErrorCodes.UNEXPECTED_EXCEPTION, e); } } return uninitializedProperties; } /** * this is called during setProperties to check after everything is setup that some properties may be null. Usually * it is not recommended to have properties not setup. But for example the RETURN properties for ComponentProperties * may be null. * * @param f field to be check if a null value is tolerated after initialization. * @return true if the null value is accepted for the given field after setup. */ protected boolean acceptUninitializedField(Field f) { return false;// by default all property need to be initialized after setup. } /** * This shall set the value holder for all the properties, set the i18n formatter of this current class to the * properties so that the i18n values are computed agains this class message properties. This calls the * initProperties for all field of type {@link Property} * * @param f field to be initialized * @param value associated with this field, never null */ protected void initializeField(Field f, NamedThing value) { // check that field name matches the NamedThing name if (!f.getName().equals(value.getName())) { throw new IllegalArgumentException("The java field [" + this.getClass().getCanonicalName() + "." + f.getName() + "] should be named identically to the instance name [" + value.getName() + "]"); } if (value instanceof Property) { // Do not set the i18N for nested Properties, they already handle their i18n value.setI18nMessageFormatter(getI18nMessageFormatter()); } else if (value instanceof PropertiesImpl) {// a property so setit up ((PropertiesImpl) value).initProperties(); } // else nothing to initialize. } private void initLayout() { if (!layoutAlreadyInitalized) {// prevent 2 initialization if the same Props instance is used in 2 comps List<NamedThing> properties = getProperties(); for (NamedThing prop : properties) { if (prop instanceof PropertiesImpl) { ((PropertiesImpl) prop).initLayout(); } // else not layout to initialize. } setupLayout(); refreshAllFormsLayout(); layoutAlreadyInitalized = true; } // else already initialized } protected void refreshAllFormsLayout() { for (Form form : getForms()) { refreshLayout(form); } } @Override public void setupProperties() { // left empty for subclass to override } @Override public void setupLayout() { // left empty for subclass to override } @Override public String toSerialized() { handleAllPropertyEncryption(ENCRYPT); try { return JsonWriter.objectToJson(this); } finally { handleAllPropertyEncryption(!ENCRYPT); } } protected static final boolean ENCRYPT = true; /** * this will look for all property with the encrypt flag including nested Properties and encrypt or decrypt them */ protected void handleAllPropertyEncryption(final boolean encrypt) { accept(new PropertyVisitor() { @Override public void visit(Property property, Properties parent) { if (property.isFlag(Property.Flags.ENCRYPT)) { property.encryptStoredValue(encrypt); } } }, null);// null } @Override public void refreshLayout(Form form) { if (form != null) { form.setRefreshUI(true); } // else nothing to refresh } @Override public List<Form> getForms() { return forms; } @Override public Form getForm(String formName) { final String wantedFormName; // default to Form.Main if the formName is blank if (StringUtils.isBlank(formName)) { wantedFormName = Form.MAIN; } else { wantedFormName = formName; } for (Form f : forms) { if (f.getName().equals(wantedFormName)) { return f; } } return null; } @Override public Form getPreferredForm(String formName) { Form form = getForm(formName); if (form != null) { return form; } // This is the only fallback case at present. If there are other fallback cases, // they can be included here. if (formName.equals(Form.CITIZEN_USER)) { return getForm(Form.MAIN); } return form; } @Override public void addForm(Form form) { forms.add(form); } @Override public List<NamedThing> getProperties() { // TODO this should be changed to AnyProperty type but it as impact everywhere List<NamedThing> properties = new ArrayList<>(); List<Field> propertyFields = getAnyPropertyFields(); for (Field f : propertyFields) { try { if (NamedThing.class.isAssignableFrom(f.getType())) { f.setAccessible(true); Object fValue = f.get(this); if (fValue != null) { NamedThing se = (NamedThing) fValue; properties.add(se); } // else not initalized but this is already handled in the initProperties that must be called // before the getProperties } } catch (IllegalAccessException e) { throw new TalendRuntimeException(CommonErrorCodes.UNEXPECTED_EXCEPTION, e); } } return properties; } /** * @return a direct list of field assignable from AnyProperty */ private List<Field> getAnyPropertyFields() { List<Field> propertyFields = new ArrayList<>(); Field[] fields = getClass().getFields(); for (Field f : fields) { if (isAPropertyType(f.getType())) { propertyFields.add(f); } } return propertyFields; } @Override public void accept(AnyPropertyVisitor visitor, Properties parent) { // uses a set that uses reference-equality instead of instance-equality to avoid stackoveflow with hashcode() using a // visitor. Set<Properties> visited = Collections.newSetFromMap(new IdentityHashMap<Properties, Boolean>()); acceptInternal(visitor, parent, visited); } private void acceptInternal(AnyPropertyVisitor visitor, Properties parent, Set<Properties> visited) { if (visited.contains(this)) { return; } visited.add(this); List<NamedThing> properties = getProperties(); for (NamedThing nt : properties) { if (nt instanceof PropertiesImpl) { ((PropertiesImpl) nt).acceptInternal(visitor, this, visited); } else if (nt instanceof AnyProperty) { ((AnyProperty) nt).accept(visitor, this); } } visitor.visit(this, parent); } /** * Is this object of type {@link Property} or {@link Properties}? * * @param clazz, the class to be tested * @return true if the clazz inherits from {@link Property} or {@link Properties}. */ protected boolean isAPropertyType(Class<?> clazz) { return AnyProperty.class.isAssignableFrom(clazz); } /** * @return a Namething from a property path wich allow to recurse into nested properties using the . as a separator * for Properties names and the final Property. Or null if none found */ @Override public NamedThing getProperty(String propPath) { if (propPath != null) { String[] propComps = propPath.split("\\."); PropertiesImpl currentProps = this; int i = 0; for (String prop : propComps) { if (i++ == propComps.length - 1) { return currentProps.getLocalProperty(prop); } NamedThing se = currentProps.getLocalProperty(prop); if (!(se instanceof PropertiesImpl)) { return null; } currentProps = (PropertiesImpl) se; } } // else propName is null so return null return null; } @Override public Property<?> getValuedProperty(String propPath) { NamedThing prop = getProperty(propPath); return (prop instanceof Property) ? (Property<?>) prop : null; } @Override public Properties getProperties(String propPath) { NamedThing prop = getProperty(propPath); return (prop instanceof Properties) ? (Properties) prop : null; } /** * Returns the property in this object specified by a the simple (unqualified) property name. * * @param propName a simple property name. Should never be null */ protected NamedThing getLocalProperty(String propName) { List<NamedThing> properties = getProperties(); for (NamedThing prop : properties) { if (propName.equals(prop.getName())) { return prop; } } return null; } @Override public void setValue(String property, Object value) { NamedThing p = getProperty(property); if (!(p instanceof Property)) { throw new IllegalArgumentException("setValue but property: " + property + " is not a Property"); } ((Property) p).setValue(value); } @Override public void setValueEvaluator(PropertyValueEvaluator ve) { List<NamedThing> properties = getProperties(); for (NamedThing prop : properties) { if (prop instanceof Property) { ((Property<?>) prop).setValueEvaluator(ve); } else if (prop instanceof Properties) { ((Properties) prop).setValueEvaluator(ve); } } } public void setValidationResult(ValidationResult vr) { validationResult = vr; } @Override public ValidationResult getValidationResult() { return validationResult; } @Override public void assignNestedProperties(Properties... newValueProperties) { List<Field> propertyFields = getAnyPropertyFields(); for (Field propField : propertyFields) { Class<?> propType = propField.getType(); if (Properties.class.isAssignableFrom(propType)) { boolean isNewAssignment = false; for (Properties newValue : newValueProperties) { if (propType.isAssignableFrom(newValue.getClass())) { try { propField.set(this, newValue); } catch (IllegalArgumentException | IllegalAccessException e) { throw new TalendRuntimeException(CommonErrorCodes.UNEXPECTED_EXCEPTION, e); } isNewAssignment = true; } // else not a compatible type so keep looking } if (!isNewAssignment) {// recurse Properties prop; try { prop = (Properties) propField.get(this); if (prop != null) { prop.assignNestedProperties(newValueProperties); } // else prop value is null so we can't recurse. this should never happend } catch (IllegalArgumentException | IllegalAccessException e) { throw new TalendRuntimeException(CommonErrorCodes.UNEXPECTED_EXCEPTION, e); } // cast is ok we check it was assignable before. } } // else not a nestedProperties so keep looking. } } @Override public void copyValuesFrom(Properties props) { copyValuesFrom(props, true, true); } @Override public void copyValuesFrom(Properties props, boolean copyTaggedValues, boolean copyEvaluators) { for (NamedThing otherProp : props.getProperties()) { NamedThing thisProp = getProperty(otherProp.getName()); if (thisProp == null) { // the current Property or Properties is null so we need to create a new instance try { thisProp = createPropertyInstance(otherProp); // assign the newly created instance to the field. try { Field f = getClass().getField(otherProp.getName()); f.set(this, thisProp); } catch (NoSuchFieldException e) { // A field exists in the other that's not in ours, just ignore it continue; } } catch (ReflectiveOperationException | SecurityException e) { TalendRuntimeException.unexpectedException(e); } } // thisProp cannot be null here. // recurse if it is a Properties if (otherProp instanceof PropertiesImpl) { ((PropertiesImpl) thisProp).copyValuesFrom((Properties) otherProp); ((PropertiesImpl) thisProp).refreshAllFormsLayout(); } else if (otherProp instanceof Property) { // copy the value Object value = ((Property) otherProp).getStoredValue(); ((Property) thisProp).setStoredValue(value); if (copyTaggedValues) { ((Property) thisProp).copyTaggedValues((Property) otherProp); } if (copyEvaluators) { ((Property) thisProp).setValueEvaluator(((Property) otherProp).getValueEvaluator()); } } else { TalendRuntimeException.unexpectedException( "The property " + otherProp.getClass().getName() + " is not of the expected type."); } } } @Override public NamedThing createPropertyInstance(NamedThing otherProp) throws ReflectiveOperationException { NamedThing thisProp = null; Class<? extends NamedThing> otherClass = otherProp.getClass(); if (Property.class.isAssignableFrom(otherClass)) { Property<?> otherPy = (Property<?>) otherProp; Constructor<? extends NamedThing> c = otherClass.getDeclaredConstructor(String.class, String.class); c.setAccessible(true); thisProp = c.newInstance(otherPy.getType(), otherPy.getName()); } else if (Properties.class.isAssignableFrom(otherClass)) { // Look for single arg String, but an inner class will have a Properties as first arg Constructor<?>[] constructors = otherClass.getConstructors(); for (Constructor<?> c : constructors) { Class<?> pts[] = c.getParameterTypes(); c.setAccessible(true); if (pts.length == 1 && String.class.isAssignableFrom(pts[0])) { thisProp = (NamedThing) c.newInstance(otherProp.getName()); break; } if (pts.length == 2 && Properties.class.isAssignableFrom(pts[0]) && String.class.isAssignableFrom(pts[1])) { thisProp = (NamedThing) c.newInstance(this, otherProp.getName()); break; } } if (thisProp == null) { TalendRuntimeException.unexpectedException( "Failed to find a proper constructor in Properties : " + otherClass.getName()); } } else { TalendRuntimeException.unexpectedException( "Unexpected property class: " + otherProp.getClass() + " prop: " + otherProp); } return thisProp; } /** * creates a new Properties instance looking for a String constructor and uses name as a parameter * * @param propClass never null, the class to instantiate. * @param name the name of the properties to be set if a String contructor is found * @throws TalendRuntimeException if any reflection method throws an exception. */ public static <P extends Properties> P createNewInstance(Class<P> propClass, String name) { try { // look for a string constructor Constructor<P> stringConstructor = propClass.getConstructor(String.class); if (stringConstructor != null) { return stringConstructor.newInstance(name); } // else no constructor found so throw an exception throw TalendRuntimeException.createUnexpectedException( "Could not find a suitable constructor for class [" + propClass.getName() + "]"); } catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { throw TalendRuntimeException.createUnexpectedException(e); } } @Override public String getName() { return name; } @Override public Properties setName(String name) { this.name = name; return this; } @Override public String getDisplayName() { return getName() != null ? getI18nMessage("properties." + getName() + I18N_DISPLAY_NAME_SUFFIX) : ""; } @Override public String getTitle() { return name; } @Override public String toString() { return toStringIndent(0); } @Override public String toStringIndent(int indent) { StringBuilder sb = new StringBuilder(); String is = ToStringIndentUtil.indentString(indent); sb.append(is + getName() + " - " + getTitle() + " " + getClass().getName()); sb.append("\n" + is + " Properties:"); for (NamedThing prop : getProperties()) { if (prop instanceof ToStringIndent) { sb.append('\n' + ((ToStringIndent) prop).toStringIndent(indent + 6)); } else { sb.append('\n' + prop.toString()); } String value = prop instanceof Property ? ((Property<?>) prop).getStringValue() : null; if (value != null) { sb.append(" [" + value + "]"); } } sb.append("\n " + is + " Forms:"); if (getForms() != null) { for (Form form : getForms()) { sb.append("\n" + form.toStringIndent(indent + 6)); } } return sb.toString(); } /** * hashcode is compute with the recursive Property name and values. */ @Override public int hashCode() { final HashCodeBuilder hashCodeBuilder = new HashCodeBuilder(17, 5); accept(new PropertyVisitor() { @Override public void visit(Property property, Properties parent) { // use the property name hashCodeBuilder.append(property.getName()); // and the property value hashCodeBuilder.append(property.getStoredValue()); } }, null); return hashCodeBuilder.toHashCode(); } @Override public boolean equals(Object obj) { if (this == obj) { return true; } if (obj == null) { return false; } if (getClass() != obj.getClass()) { return false; } PropertiesImpl other = (PropertiesImpl) obj; return computeEqualityWith(this, other); } boolean computeEqualityWith(Properties current, Properties other) { final EqualsBuilder equalsBuilder = new EqualsBuilder(); List<NamedThing> properties = current.getProperties(); for (NamedThing nt : properties) { if (nt instanceof PropertiesImpl) { equalsBuilder.append(nt, other.getProperties(nt.getName())); } else if (nt instanceof Property<?>) { equalsBuilder.append(nt, other.getValuedProperty(nt.getName())); } } return equalsBuilder.isEquals(); } }