/* * Carrot2 project. * * Copyright (C) 2002-2016, Dawid Weiss, Stanisław Osiński. * All rights reserved. * * Refer to the full license file "carrot2.LICENSE" * in the root folder of the repository checkout or at: * http://www.carrot2.org/carrot2.LICENSE */ package org.carrot2.workbench.editors.factory; import static org.apache.commons.lang.ClassUtils.*; import java.lang.annotation.Annotation; import java.util.*; import org.carrot2.core.IProcessingComponent; import org.carrot2.util.attribute.AttributeDescriptor; import org.carrot2.workbench.editors.IAttributeEditor; import org.carrot2.shaded.guava.common.base.Predicate; import org.carrot2.shaded.guava.common.collect.*; /** * See {@link #getEditorFor(Class, AttributeDescriptor)}. */ public final class EditorFactory { /* * */ private EditorFactory() { // no instances. } /** * Return the best matching {@link IAttributeEditor} for a given * {@link AttributeDescriptor} and {@link IProcessingComponent}. * * @param componentClazz Component class or <code>null</code> if no specific component is * available and generic editor should be returned. * * @throws EditorNotFoundException If no editor for a given attribute could be found. */ public static IAttributeEditor getEditorFor( Class<? extends IProcessingComponent> componentClazz, AttributeDescriptor attribute) { IAttributeEditor editor = null; if (componentClazz != null) { editor = findDedicatedEditor(componentClazz, attribute); } if (editor == null) { editor = findGenericEditor(attribute); } if (editor == null) { throw new EditorNotFoundException("No suitable editor found for attribute: " + attribute.toString()); } return editor; } /** * Find a generic attribute editor for a given type. */ private static IAttributeEditor findGenericEditor(AttributeDescriptor attribute) { List<TypeEditorWrapper> typeCandidates = getCompatibleTypeEditors(attribute); if (!typeCandidates.isEmpty()) { typeCandidates = sortTypeEditors(typeCandidates, attribute); return typeCandidates.get(0).getExecutableComponent(); } return null; } /** * Find a {@link IProcessingComponent}-dedicated attribute editor. */ private static IAttributeEditor findDedicatedEditor( final Class<? extends IProcessingComponent> clazz, final AttributeDescriptor attribute) { List<DedicatedEditorWrapper> candidates = getCompatibleDedicatedEditors(clazz, attribute); if (!candidates.isEmpty()) { return candidates.get(0).getExecutableComponent(); } return null; } /** * Sort attribute editors based on various criteria (type proximity, number * of matching and available constraints). */ private static List<TypeEditorWrapper> sortTypeEditors( List<TypeEditorWrapper> editors, final AttributeDescriptor attribute) { final List<String> annotationNames = Lists.newArrayList(); for (Annotation ann : attribute.constraints) { annotationNames.add(ann.annotationType().getName()); } final HashMap<TypeEditorWrapper, Integer> matchingConstraints = Maps.newHashMap(); for (TypeEditorWrapper t : editors) { List<String> matches = Lists.newArrayList(annotationNames); matches.retainAll(t.constraints); matchingConstraints.put(t, matches.size()); } final Comparator<TypeEditorWrapper> comparator = new Comparator<TypeEditorWrapper>() { public int compare(TypeEditorWrapper o1, TypeEditorWrapper o2) { int result = distance(attribute.type, o1.attributeClass) - distance(attribute.type, o2.attributeClass); /* * Consult the number of matching constraints and pick the more specific * editor (with more available constraints). */ if (result == 0) { result = - (matchingConstraints.get(o1) - matchingConstraints.get(o2)); } /* * Consult again in case of a draw and pick the editor that has more optional * constraints (even if they are not present). */ if (result == 0) { result = - (o1.constraints.size() - o2.constraints.size()); } return result; } }; return Ordering.from(comparator).sortedCopy(editors); } /** * Return a list of {@link TypeEditorWrapper} compatible with an {@link AttributeDescriptor}. */ private static List<TypeEditorWrapper> getCompatibleTypeEditors( final AttributeDescriptor attribute) { final List<String> annotationNames = Lists.newArrayList(); for (Annotation ann : attribute.constraints) { annotationNames.add(ann.annotationType().getName()); } return AttributeEditorLoader.INSTANCE .filterTypeEditors(new Predicate<TypeEditorWrapper>() { public boolean apply(TypeEditorWrapper editor) { boolean result = isCompatible(attribute.type, editor.attributeClass); /* * For editors with constraints, check allConstraintsRequired condition. */ if (result && !editor.constraints.isEmpty() && editor.allConstraintsRequired) { result = annotationNames.containsAll(editor.constraints); } return result; } }); } /** * Return a list of compatible {@link DedicatedEditorWrapper}. There should * be zero or at most one. */ private static List<DedicatedEditorWrapper> getCompatibleDedicatedEditors( final Class<? extends IProcessingComponent> clazz, final AttributeDescriptor attribute) { return AttributeEditorLoader.INSTANCE .filterDedicatedEditors(new Predicate<DedicatedEditorWrapper>() { public boolean apply(DedicatedEditorWrapper editor) { return isCompatible(clazz, editor.componentClass) && editor.attributeId.equals(attribute.key); } }); } /** * Return <code>true</code> if a given <code>className<code> is assignable * to <code>clazz</code>. */ @SuppressWarnings("unchecked") private static boolean isCompatible(Class<?> clazz, String className) { /* * This checking is currently class-name based instead of using * runtime-type information (assignability). * Is this because of class-loader problems? */ boolean compatible = clazz.getName().equals(className); if (!compatible) { List<String> superClasses = convertClassesToClassNames(getAllSuperclasses(clazz)); compatible = superClasses.contains(className); } if (!compatible && clazz.isInterface()) { compatible = "java.lang.Object".equals(className); } if (!compatible) { List<String> interfaces = convertClassesToClassNames(getAllInterfaces(clazz)); compatible = interfaces.contains(className); } return compatible; } /** * Return the <i>distance</i> between a given <code>className</code> and * <code>clazz</code>. The distance is calculated based on the difference in the * number of classes in the hierarchy of inheritance. */ @SuppressWarnings("unchecked") public static int distance(Class<?> clazz, String className) { if (clazz.getName().equals(className)) { return 0; } /* * Matches everything, but is least specific. */ if (className.equals("java.lang.Object")) { return Integer.MAX_VALUE; } int distance = Integer.MAX_VALUE; /* * Interface match. */ List<String> interfaces = convertClassesToClassNames(getAllInterfaces(clazz)); if (interfaces.contains(className)) { distance = Math.min(1, distance); } /* * Superclass match. */ List<String> superclasses = convertClassesToClassNames(getAllSuperclasses(clazz)); if (superclasses.contains(className)) { distance = Math.min(superclasses.indexOf(className) + 1, distance); } if (distance == Integer.MAX_VALUE) { throw new RuntimeException("Cannot calculate distance between incompatible classes: " + clazz + ", " + className); } return distance; } }