/* * Copyright (c) 2007 BUSINESS OBJECTS SOFTWARE LIMITED * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, * this list of conditions and the following disclaimer. * * * Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * * Neither the name of Business Objects nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. */ /* * IntellicutListModelAdapter.java * Creation date: Dec 12, 2003 * By: Frank Worsley */ package org.openquark.gems.client; import java.util.Arrays; import java.util.Collections; import java.util.Comparator; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.SortedSet; import java.util.TreeSet; import javax.swing.JComponent; import org.openquark.cal.compiler.FieldName; import org.openquark.cal.compiler.ModuleTypeInfo; import org.openquark.cal.compiler.QualifiedName; import org.openquark.cal.compiler.ScopedEntityNamingPolicy; import org.openquark.cal.compiler.SourceMetrics; import org.openquark.cal.compiler.TypeExpr; import org.openquark.cal.compiler.ScopedEntityNamingPolicy.UnqualifiedUnlessAmbiguous; import org.openquark.cal.compiler.TypeChecker.TypeCheckInfo; import org.openquark.cal.services.CALWorkspace; import org.openquark.cal.services.GemEntity; import org.openquark.cal.services.GemViewer; import org.openquark.cal.services.MetaModule; import org.openquark.cal.services.Perspective; import org.openquark.cal.services.UnorderedTypeMatchFilter; import org.openquark.gems.client.AutoburnLogic.AutoburnUnifyStatus; import org.openquark.gems.client.AutoburnLogic.BurnCombination; import org.openquark.gems.client.IntellicutListModel.FilterLevel; import org.openquark.gems.client.IntellicutManager.IntellicutInfo; /** * An adapter class for the IntellicutListModel. Specific implementations of an adapter derive * from this class and implement the abstract methods. This class allows the IntellicutListModel * to be customized to work with additional data types. * @author Frank Worsley */ public abstract class IntellicutListModelAdapter { /** * Comparator class used to do a compare between IntellicutListEntry objects. */ private static class IntellicutListEntryComparator implements Comparator<Object> { private static final CaseInsensitiveStringComparator stringComparator = new CaseInsensitiveStringComparator(); /** * Case insensitive string comparator. This is identical to the case insensitive * comparator in the java.lang.String class, except that it bumps the importance * of strings beginning with "{" to be ordered right after strings beginning with "(". */ private static class CaseInsensitiveStringComparator implements Comparator<String> { public int compare(String s1, String s2) { int n1 = s1.length(), n2 = s2.length(); if (n1 > 0 && n2 > 0) { if (s1.charAt(0) == '{') { char c2 = s2.charAt(0); if (c2 <= '(') { return 1; } else if (c2 != '{') { return -1; } else { s1 = s1.substring(1); s2 = s2.substring(1); n1--; n2--; } } else if (s2.charAt(0) == '{') { char c1 = s1.charAt(0); if (c1 <= '(') { return -1; } else if (c1 != '{') { return 1; } else { s1 = s1.substring(1); s2 = s2.substring(1); n1--; n2--; } } } for (int i1 = 0, i2 = 0; i1 < n1 && i2 < n2; i1++, i2++) { char c1 = s1.charAt(i1); char c2 = s2.charAt(i2); if (c1 != c2) { c1 = Character.toUpperCase(c1); c2 = Character.toUpperCase(c2); if (c1 != c2) { c1 = Character.toLowerCase(c1); c2 = Character.toLowerCase(c2); if (c1 != c2) { return c1 - c2; } } } } return n1 - n2; } } /** Map to hold ordering of intellicut list entry classes */ private static final Map<String, Integer> classOrderMap = new HashMap<String, Integer>(); static { classOrderMap.put( GemEntityListEntry.class.toString(), Integer.valueOf(0)); classOrderMap.put(CollectorGemListEntry.class.toString(), Integer.valueOf(1)); classOrderMap.put( TypeExprListEntry.class.toString(), Integer.valueOf(2)); } /** * Compares the list entry objects. * The entries are first ordered by alphabetically ascending sort strings, * sub-ordered by alphabetically ascending display strings, sub-ordered by * class type and contents. * All string comparisons are case insensitive, and the class orderings are * defined in the classOrderMap. * * @param o1 * @param o2 * @return negative, zero, or positive integer, when the first object * is prior to, the same as, or greater than the second object */ public int compare (Object o1, Object o2) { // Compare sorted strings String s1 = ((IntellicutListEntry) o1).getSortString(); String s2 = ((IntellicutListEntry) o2).getSortString(); int c = stringComparator.compare(s1, s2); if (c == 0) { // Sorted strings are equal, so compare their display strings s1 = ((IntellicutListEntry) o1).getDisplayString(); s2 = ((IntellicutListEntry) o2).getDisplayString(); c = stringComparator.compare(s1, s2); } if (c == 0) { // Display strings are equal, so compare their secondary display strings s1 = ((IntellicutListEntry) o1).getSecondaryDisplayString(); s2 = ((IntellicutListEntry) o2).getSecondaryDisplayString(); c = stringComparator.compare(s1, s2); } if (c == 0) { // Secondary display strings are equal, so compare the classes c = classOrderMap.get(o1.getClass().toString()).compareTo(classOrderMap.get(o2.getClass().toString())); } if (c == 0) { // Classes are equal, so compare their string representations c = o1.toString().compareTo(o2.toString()); } return c; } } /** * This class represents an entry in the intellicut model. It encapsulates the data object * this entry represents and the intellicut info of that data object. * @author Frank Worsley */ public static abstract class IntellicutListEntry { /** The data object that this list entry encapsulates. */ private final Object dataObject; /** The intellicut info associated with this list entry. */ private IntellicutInfo intellicutInfo; /** * Constructor for a new IntellicutListEntry object. * @param dataObject the data object this list entry is for */ public IntellicutListEntry(Object dataObject) { if (dataObject == null) { throw new NullPointerException(); } this.dataObject = dataObject; } /** * @return the data object held onto by this list entry */ public final Object getData() { return dataObject; } /** * @param intellicutInfo the intellicut info for this list entry (must be non-null) */ public void setIntellicutInfo(IntellicutInfo intellicutInfo) { if (intellicutInfo == null) { throw new NullPointerException(); } this.intellicutInfo = intellicutInfo; } /** * @return the intellicut info for this list entry */ public IntellicutInfo getIntellicutInfo() { return intellicutInfo; } /** * @return the string representation of the entry as it should be displayed in the list. */ public abstract String getDisplayString(); /** * @return the string representation of the entry as it should be written in the source code. */ public String getSourceText(){ String sourceText = getSourceText(); Object entryData = getData(); if (entryData instanceof GemEntity) { if (!getSecondaryDisplayString().equals("")) { // Secondary display string exists; this indicates the module for an ambiguous entry sourceText = ((GemEntity)entryData).getName().getQualifiedName(); } } return sourceText; } /** * @return a string containing additional information which is subjected to * special formatting when displayed in the list. Never null. */ public String getSecondaryDisplayString() { return ""; } /** * @return the string to use for sorting this entry in the intellicut list. This should * always be the unqualified name of the entry. This name is also used for narrowing * the intellicut list if the user types. */ public abstract String getSortString(); /** * @return the result type of the stored object or null if there is no result type. */ public abstract TypeExpr getResultType(); /** * @return the type expression of the stored object or null if there is no type expression */ public abstract TypeExpr getTypeExpr(); /** * @return the type pieces of the stored object to use for autoburning or null if * there are no such type pieces. */ public abstract TypeExpr[] getTypePiecesForBurning(); /** * @return the argument type pieces of the stored object or null if there are no such type pieces. */ public abstract TypeExpr[] getArgumentTypePieces(); /** * @return true if the stored object has any disconnected inputs, false if all inputs * are connected or if there are no inputs at all. */ public abstract boolean hasDisconnectedInputs(); /** * @return whether autoburning should be attempted on the stored object */ public abstract boolean isAutoBurnable(); /** * We need to override equals so we can find list entries after the list has been * reloaded and restore the list position. Two list entries are equal if they * have the same data object. * @param o the object to compare this list entry with * @return true if this list entry equals the list entry referenced by object o */ @Override public boolean equals(Object o) { if (o == this) { return true; } if (o instanceof IntellicutListEntry) { IntellicutListEntry listEntry = (IntellicutListEntry) o; if (listEntry.getData().equals(dataObject)) { return true; } } return false; } /** * The hashcode for a list entry is the hash code of its stored data object * This is overridden so that it corresponds with the equals implementation. * @return the hash code for this list entry */ @Override public int hashCode() { return dataObject.hashCode(); } /** * Don't remove this. It's used by the IntellicutComboBox to display the text of an * entry in the combobox text field. * @return the String representation of the list entry as it should be displayed in the list */ @Override public String toString() { return getDisplayString(); } } /** * A list entry implementation for a GemEntity. * @author Frank Worsley */ private static class GemEntityListEntry extends IntellicutListEntry { /** The naming policy to use when displaying the entity name. */ private ScopedEntityNamingPolicy namingPolicy; public GemEntityListEntry(GemEntity entity, ScopedEntityNamingPolicy namingPolicy) { super(entity); if (namingPolicy == null) { throw new NullPointerException(); } this.namingPolicy = namingPolicy; } public GemEntity getEntity() { return (GemEntity) getData(); } @Override public String getDisplayString() { return getEntity().getName().getUnqualifiedName(); } @Override public String getSourceText(){ return getEntity().getAdaptedName(namingPolicy); } /** * {@inheritDoc} */ @Override public String getSecondaryDisplayString() { String adaptedName = getEntity().getAdaptedName(namingPolicy); if (QualifiedName.isValidCompoundName(adaptedName)) { // Adapted name is fully qualified, so entry is ambiguous // (use module as secondary display string) QualifiedName qualifiedName = QualifiedName.makeFromCompoundName(adaptedName); return qualifiedName.getModuleName().toSourceText(); } else { // Unqualified name; no additional info return ""; } } @Override public String getSortString() { return getEntity().getName().getUnqualifiedName(); } @Override public TypeExpr getTypeExpr() { return getEntity().getTypeExpr(); } @Override public TypeExpr getResultType() { return getEntity().getTypeExpr().getResultType(); } @Override public TypeExpr[] getTypePiecesForBurning() { return getEntity().getTypeExpr().getTypePieces(); } @Override public TypeExpr[] getArgumentTypePieces() { TypeExpr typeExpr = getEntity().getTypeExpr(); int numArgs = typeExpr.getArity(); if (numArgs == 0) { return null; } else { TypeExpr arguments[] = new TypeExpr[numArgs]; TypeExpr pieces[] = typeExpr.getTypePieces(); System.arraycopy(pieces, 0, arguments, 0, numArgs); return arguments; } } @Override public boolean hasDisconnectedInputs() { return getEntity().getTypeArity() > 0; } @Override public boolean isAutoBurnable() { return true; } void setNamingPolicy(ScopedEntityNamingPolicy namingPolicy) { this.namingPolicy = namingPolicy; } } /** * A list entry implementation for a TypeExpr. * @author Frank Worsley */ private static class TypeExprListEntry extends IntellicutListEntry { /** The naming policy to use when displaying the entity name. */ private final ScopedEntityNamingPolicy namingPolicy; public TypeExprListEntry(TypeExpr typeExpr, ScopedEntityNamingPolicy namingPolicy) { super(typeExpr); if (namingPolicy == null) { throw new NullPointerException(); } this.namingPolicy = namingPolicy; } @Override public String getDisplayString() { // Special case: Record polymorphic types are displayed as {Record} rather than {r} if (getTypeExpr().sameType(TypeExpr.makeFreeRecordType(Collections.<FieldName>emptySet()))) { return "{Record}"; } else { return getTypeExpr().toString(namingPolicy); } } @Override public String getSortString() { return getTypeExpr().toString(namingPolicy); } @Override public TypeExpr getTypeExpr() { return (TypeExpr) getData(); } @Override public TypeExpr getResultType() { return getTypeExpr().getResultType(); } @Override public TypeExpr[] getTypePiecesForBurning() { return getTypeExpr().getTypePieces(); } @Override public TypeExpr[] getArgumentTypePieces() { int numArgs = getTypeExpr().getArity(); if (numArgs == 0) { return null; } else { TypeExpr arguments[] = new TypeExpr[numArgs]; TypeExpr pieces[] = getTypeExpr().getTypePieces(); System.arraycopy(pieces, 0, arguments, 0, numArgs); return arguments; } } @Override public boolean hasDisconnectedInputs() { return getTypeExpr().getArity() > 0; } @Override public boolean isAutoBurnable() { return true; } } /** * A list entry implementation for a CollectorGem. * @author Frank Worsley */ private static class CollectorGemListEntry extends IntellicutListEntry { public CollectorGemListEntry(CollectorGem collector) { super(collector); } public CollectorGem getCollector() { return (CollectorGem) getData(); } @Override public String getDisplayString() { return getCollector().getUnqualifiedName(); } @Override public String getSortString() { return getCollector().getUnqualifiedName(); } @Override public TypeExpr getResultType() { return getCollector().getCollectingPart().getType(); } @Override public TypeExpr getTypeExpr() { return getCollector().getCollectingPart().getType(); } @Override public TypeExpr[] getTypePiecesForBurning() { return null; } @Override public TypeExpr[] getArgumentTypePieces() { return getCollector().getCollectingPart().getType().getTypePieces(); } @Override public boolean hasDisconnectedInputs() { return false; } @Override public boolean isAutoBurnable() { return false; } } /** * An adapter that includes all visible gems from the perspective for that can * match given input AND output types. * @author Frank Worsley */ public static class InputOutputAdapter extends IntellicutListModelAdapter { /** The type check info to use for autoburning. */ private final TypeCheckInfo typeCheckInfo; /** The workspace to load modules from. */ private final CALWorkspace workspace; /** List of valid input types that gems must support. */ private TypeExpr[] inputExpr = null; /** List of valid output types that gems must support. */ private TypeExpr[] outputExpr = null; /** * Constructor for the InputOuputAdapter which is used by the IntellicutComboBox. * * @param inputTypes a list of valid input types to the gems * @param outputTypes a list of valid output types to the gems * @param workspace the workspace to load gems from * @param typeCheckInfo the type check info to use */ public InputOutputAdapter(TypeExpr[] inputTypes, TypeExpr[] outputTypes, CALWorkspace workspace, TypeCheckInfo typeCheckInfo) { if (workspace == null || typeCheckInfo == null) { throw new NullPointerException(); } this.inputExpr = inputTypes.clone(); this.outputExpr = outputTypes.clone(); this.typeCheckInfo = typeCheckInfo; this.workspace = workspace; setNamingPolicy(new UnqualifiedUnlessAmbiguous(typeCheckInfo.getModuleTypeInfo())); } @Override protected Set<GemEntity> getDataObjects() { MetaModule targetMetaModule = workspace.getMetaModule(typeCheckInfo.getModuleName()); GemViewer gemViewer = new GemViewer(); gemViewer.addFilter(new UnorderedTypeMatchFilter(inputExpr, outputExpr, targetMetaModule.getTypeInfo(), true)); Perspective perspective = new Perspective(workspace, targetMetaModule); perspective.setGemEntityViewer(gemViewer); return getVisibleGemsFromPerspective(perspective); } @Override protected IntellicutInfo getIntellicutInfo(IntellicutListEntry listEntry) { // If only output type restrictions have been specified, then calculate type // type closeness of the results. if (inputExpr == null && outputExpr != null && outputExpr.length > 0) { return getIntellicutInfo(listEntry, outputExpr, typeCheckInfo, true, workspace.getSourceMetrics()); } else { return new IntellicutInfo(AutoburnUnifyStatus.NOT_NECESSARY, -1, 1, false, getReferenceFrequency(workspace.getSourceMetrics(), listEntry)); } } /** * Resets this adapter by clearing all loaded gems and updating the input/output * types to use. Next time the model that uses this adapter is refreshed, it will * cause the adapter to reload the model. * @param inputExpr a list of valid input types to the gems * @param outputExpr a list of valid output types from the gems */ public void reset(TypeExpr[] inputExpr, TypeExpr[] outputExpr) { this.inputExpr = inputExpr.clone(); this.outputExpr = outputExpr.clone(); clear(); } } /** * The adapter used by the IntellicutList for the TableTop. It includes all entities that * can connect to a given part connectable. * @author Frank Worsley */ public static class PartConnectableAdapter extends IntellicutListModelAdapter { /** The intellicut part we are building the list for. */ private final Gem.PartConnectable intellicutPart; /** The type check info to use for autoburning. */ private final TypeCheckInfo typeCheckInfo; /** The perspective we loaded the gems from. */ private final Perspective perspective; /** * Constructor for IntellicutListModel used by the IntellicutList. The PartConnectable passed in * will be used to find a list of gems that can connect to that part. * * If the connectable is a PartInput then all gems whose output (maybe with burning) can connect to * that input will be included. If the connectable is a PartOutput then all gems whose input can * connect with the connectable will be included. If the connectable is null then all gems will be * included in the list. * * @param intellicutPart the PartConnectable to match gems with * @param perspective the perspective to get gems from * @param typeCheckInfo the type check info to use for autoburning */ public PartConnectableAdapter(Gem.PartConnectable intellicutPart, Perspective perspective, TypeCheckInfo typeCheckInfo) { if (perspective == null || typeCheckInfo == null) { throw new NullPointerException(); } this.perspective = perspective; this.intellicutPart = intellicutPart; this.typeCheckInfo = typeCheckInfo; setNamingPolicy(new UnqualifiedUnlessAmbiguous(perspective.getWorkingModuleTypeInfo())); } @Override protected Set<GemEntity> getDataObjects() { return getVisibleGemsFromPerspective(perspective); } @Override protected IntellicutInfo getIntellicutInfo(IntellicutListEntry listEntry) { if (intellicutPart instanceof Gem.PartInput) { return getIntellicutInfo(listEntry, intellicutPart.getType(), typeCheckInfo, true, perspective.getWorkspace().getSourceMetrics()); } else if (intellicutPart instanceof Gem.PartOutput) { return getIntellicutInfo(listEntry, intellicutPart.getType(), typeCheckInfo.getModuleTypeInfo(), perspective.getWorkspace().getSourceMetrics()); } else { return new IntellicutInfo(AutoburnUnifyStatus.NOT_NECESSARY, -1, 1, false, getReferenceFrequency(perspective.getWorkspace().getSourceMetrics(), listEntry)); } } /** * Resolves ambiguity between displayed names of two entities. * Clashes may occur between the displayed unqualified names of * Collector and Gem, or Gem and Gem entries; these will be resolved * by fully qualifying the names of Gem entries. * * @see org.openquark.gems.client.IntellicutListModelAdapter#resolveDisplayAmbiguity(org.openquark.gems.client.IntellicutListModelAdapter.IntellicutListEntry, org.openquark.gems.client.IntellicutListModelAdapter.IntellicutListEntry) */ @Override protected void resolveDisplayAmbiguity(IntellicutListEntry newEntry, IntellicutListEntry existingEntry) { if (newEntry instanceof CollectorGemListEntry) { if (existingEntry instanceof GemEntityListEntry) { // Collector clashes with an existing entity // Collector always has unqualified name as its displayed name, // thus the existing entry is fully qualifiable ((GemEntityListEntry)existingEntry).setNamingPolicy(ScopedEntityNamingPolicy.FULLY_QUALIFIED); } } else if (newEntry instanceof GemEntityListEntry) { if (existingEntry instanceof GemEntityListEntry) { // Gem vs Gem // Qualify both parties if they are not qualified GemEntityListEntry existingGemEntry = ((GemEntityListEntry)existingEntry); if (!QualifiedName.isValidCompoundName(existingGemEntry.getDisplayString())) { existingGemEntry.setNamingPolicy(ScopedEntityNamingPolicy.FULLY_QUALIFIED); ((GemEntityListEntry)newEntry).setNamingPolicy(ScopedEntityNamingPolicy.FULLY_QUALIFIED); } } else if (existingEntry instanceof CollectorGemListEntry) { // Gem vs Collector // Collector always has unqualified name, so qualify the gem ((GemEntityListEntry)newEntry).setNamingPolicy(ScopedEntityNamingPolicy.FULLY_QUALIFIED); } } } /** * Filters gems based on reference frequency and type closeness if possible. * If there is no type to compare for type closeness, then uses the inherited * reference-frequency-only filter. * * @param listEntry The entry to check against the specified filter * @param filterLevel The level of filter to apply * @return true if the specified list entry passes the specified filter */ @Override public boolean passesFilter(IntellicutListEntry listEntry, FilterLevel filterLevel) { if (filterLevel == null) { throw new NullPointerException("filterLevel must not be null in IntellicutListModelAdapter.PartConnectableAdapter.passesFilter"); } // If we have no current part (and therefore can't do type closeness checks), // then defer to the default filtering (which only uses reference frequency) if (intellicutPart == null) { return super.passesFilter(listEntry, filterLevel); } // Filter gems based on both reference frequency and type closeness if (filterLevel == FilterLevel.SHOW_BEST) { if (listEntry.getIntellicutInfo().isSameNonpolymorphicType()) { return listEntry.getIntellicutInfo().isSameNonpolymorphicTypeThreshold() || listEntry.getIntellicutInfo().isReferenceFrequencyInTopFifth(); } else { return (listEntry.getIntellicutInfo().getMaxTypeCloseness() >= 1) && (listEntry.getIntellicutInfo().isReferenceFrequencyInTopFifth()); } } else if (filterLevel == FilterLevel.SHOW_LIKELY) { return (listEntry.getIntellicutInfo().getMaxTypeCloseness() >= 1) || (listEntry.getIntellicutInfo().isReferenceFrequencyInTopFifth()); } else { // SHOW_ALL return true; } } } /** * Gems are divided into those whose reference frequency is in the top * REFERENCE_FREQUENCY_PERCENTILE_THRESHOLD percent, and those whose * frequency is not. */ private static final int REFERENCE_FREQUENCY_PERCENTILE_THRESHOLD = 20; /** * The top SAME_NONPOLYMORPHIC_TYPE_REFERENCE_FREQUENCY_RANK gems with the * same non-polymorphic type as the target gem are included in the Best Gems * list even if they are not in the top REFERENCE_FREQUENCY_PERCENTILE_THRESHOLD * of gems by reference frequency. (See IntellicutInfo for a description of why). */ private static final int SAME_NONPOLYMORPHIC_TYPE_REFERENCE_FREQUENCY_RANK = 10; /** * This set contains all the list entries that can be shown in the intellicut list. * That is the combination of entries for the entities from the perspective and entries * for the additional gems that have been added. Not all entries in this set may be * visible at all times, this depends on the show all/show best gems setting and the * prefix that the user has typed in. */ private final SortedSet<IntellicutListEntry> intellicutEntrySet = new TreeSet<IntellicutListEntry>(new IntellicutListEntryComparator()); /** * A map from a data object to the associated IntellicutListEntry. */ private final Map<Object, IntellicutListEntry> intellicutEntryMap = new HashMap <Object,IntellicutListEntry>(); /** * A set of additional objects that should be added to the model as list entries. */ private final Set<Gem> additionalDataObjects = new HashSet<Gem>(); /** The visibility policy to use to decide which items to display. */ private FeatureVisibilityPolicy visibilityPolicy = FeatureVisibilityPolicy.getDefault(); /** The naming policy to use to display scoped entity names. */ private ScopedEntityNamingPolicy namingPolicy = ScopedEntityNamingPolicy.FULLY_QUALIFIED; /** * Returns the set of all available data objects that will be used to create * list items. Subclasses override this to provide whatever set of objects * they want. Note that a corresponding list entry type for the data type must * exist, otherwise an exception will be thrown when the list entries are created. * @return Set of Object */ protected abstract Set<?> getDataObjects(); /** * Returns new IntellicutInfo for a given list entry. If the entry should * not appear in the list, then this method should return null. Subclasses override * this to not create list items for entities they do not want to appear. * @param listEntry the list entry for which to get intellicut info * @return the intellicut info for the given list entry */ protected abstract IntellicutInfo getIntellicutInfo(IntellicutListEntry listEntry); /** * Returns a new IntellicutListEntry for a data object. The default implementation * supports GemEntity, CollectorGem and TypeExpr data object types. Subclasses override * this if they want to add support for additional data object types. * @param dataObject the data object to get a list entry for * @return the list entry for the data object. This method should never return null. * @throws IllegalArgumentException if the data object type is not supported */ protected IntellicutListEntry getNewListEntry(Object dataObject) { if (dataObject instanceof GemEntity) { return new GemEntityListEntry((GemEntity) dataObject, namingPolicy); } else if (dataObject instanceof CollectorGem) { return new CollectorGemListEntry((CollectorGem) dataObject); } else if (dataObject instanceof TypeExpr) { return new TypeExprListEntry((TypeExpr) dataObject, namingPolicy); } throw new IllegalArgumentException("data object not supported: " + dataObject); } /** * Filters gems based on reference frequency only. Gems whose reference frequency * are in the top fifth are both Likely and Best. * * @param listEntry the list entry to check * @param filterLevel the filter level to use * @return whether or not the given list entry passes the specified filter */ public boolean passesFilter(IntellicutListEntry listEntry, FilterLevel filterLevel) { if (filterLevel == null) { throw new NullPointerException("filterLevel must not be null in IntellicutListModelAdapter.passesFilter"); } // Filter based on reference frequency if (filterLevel == FilterLevel.SHOW_ALL) { return true; } else { return listEntry.getIntellicutInfo().isReferenceFrequencyInTopFifth(); } } /** * @param namingPolicy the naming policy to use to display scoped entity names */ public void setNamingPolicy(ScopedEntityNamingPolicy namingPolicy) { this.namingPolicy = namingPolicy; } /** * @return the naming policy used by this adapter to display scoped entity names */ public ScopedEntityNamingPolicy getNamingPolicy() { return namingPolicy; } /** * @param visibilityPolicy the visibility policy to use to decide what items to display */ public void setVisibilityPolicy(FeatureVisibilityPolicy visibilityPolicy) { this.visibilityPolicy = visibilityPolicy; } /** * @return the visibility policy used by this adapter to decide what items to display */ public FeatureVisibilityPolicy getVisibilityPolicy() { return visibilityPolicy; } /** * Includes the set of specified object in the list of data object to be loaded by the model. * This must be called before the model is loaded the first time. * @param additionalDataObjects a set of Gems or GemEntities to be added */ public void addAdditionalDataObjects(Set<? extends Gem> additionalDataObjects) { this.additionalDataObjects.addAll(additionalDataObjects); } /** * Clears all entries that have been loaded. Calling this will cause the adapter * to reload entries the next time the model that uses it is refreshed. */ public void clear() { intellicutEntrySet.clear(); intellicutEntryMap.clear(); } /** * Performs the actual loading of the list items. */ public void load() { // Clear previously loaded stuff clear(); // Load entries for entities from the perspective. loadListEntries(getDataObjects()); loadListEntries(additionalDataObjects); } /** * Loads new list entries from a given data set and puts them into the result collection. * This method also resolves ambiguities between entities with same displayed string. * @param sourceDataSet the source data set from which to create them */ private void loadListEntries(Set<?> sourceDataSet) { SortedSet<IntellicutListEntry> additionalEntrySet = new TreeSet<IntellicutListEntry>(intellicutEntrySet.comparator()); int[] referenceFrequencies = new int[sourceDataSet.size()]; int[] sameNonpolymorphicTypeReferenceFrequencies = new int[sourceDataSet.size()]; Map<Object, IntellicutListEntry> additionalEntryMap = new HashMap<Object, IntellicutListEntry>(); // First gather a list of all the IntellicutInfo objects, keeping track // of the reference frequencies of each in referenceFrequencies (so that // we can sort them later) Arrays.fill(referenceFrequencies, 0); Arrays.fill(sameNonpolymorphicTypeReferenceFrequencies, 0); int numReferenceFrequencies = 0; int numSameNonpolymorphicType = 0; for (final Object dataObject : sourceDataSet) { IntellicutListEntry listEntry = getNewListEntry(dataObject); IntellicutInfo info = getIntellicutInfo(listEntry); if (info != null) { listEntry.setIntellicutInfo(info); additionalEntrySet.add(listEntry); additionalEntryMap.put(listEntry.getData(), listEntry); referenceFrequencies[numReferenceFrequencies] = info.getReferenceFrequency(); numReferenceFrequencies++; if (info.isSameNonpolymorphicType()) { sameNonpolymorphicTypeReferenceFrequencies[numSameNonpolymorphicType] = info.getReferenceFrequency(); numSameNonpolymorphicType++; } } } // Sort reference frequencies to determine the 80th percentile Arrays.sort(referenceFrequencies); int referenceFrequencyThresholdIdx = (referenceFrequencies.length-1) - (numReferenceFrequencies/(100 / REFERENCE_FREQUENCY_PERCENTILE_THRESHOLD)); // Sort reference frequencies of the gems with the same nonpolymorphic type to // determine the threshold for the top 10 values (if there are more than 10 values) int sameNonpolymorphicTypeReferenceFrequencyThreshold = 0; if (numSameNonpolymorphicType > SAME_NONPOLYMORPHIC_TYPE_REFERENCE_FREQUENCY_RANK) { Arrays.sort(sameNonpolymorphicTypeReferenceFrequencies); sameNonpolymorphicTypeReferenceFrequencyThreshold = sameNonpolymorphicTypeReferenceFrequencies[(sameNonpolymorphicTypeReferenceFrequencies.length - 1) - 10]; } // Step through again to normalize the heuristic outputs for (final IntellicutListEntry listEntry : additionalEntrySet) { listEntry.getIntellicutInfo().setTopFifthThresholds(referenceFrequencies[referenceFrequencyThresholdIdx]); listEntry.getIntellicutInfo().setSameNonpolymorphicTypeReferenceFrequencyThreshold(sameNonpolymorphicTypeReferenceFrequencyThreshold); } if (intellicutEntrySet.isEmpty()) { intellicutEntrySet.addAll(additionalEntrySet); return; } // Entries in the original set may be modified, and this set tracks them SortedSet<IntellicutListEntry> modifiedOriginalEntries = new TreeSet<IntellicutListEntry>(intellicutEntrySet.comparator()); // Lazily iterate through the existing entry set, while progressing // through the new entries. Iterator<IntellicutListEntry> o = intellicutEntrySet.iterator(); IntellicutListEntry originalEntry = o.next(); for (final IntellicutListEntry newEntry : additionalEntrySet) { // Hop over original entries until we reach our a sort string equal/greater // than that of the new entry's. while (o.hasNext() && newEntry.getSortString().compareToIgnoreCase(originalEntry.getSortString()) > 0) { originalEntry = o.next(); } // If sort strings are equal, then there is a possibility the display strings are equal // Note: Displayed strings will not be equal if sort strings are different. if ((newEntry.getSortString().compareToIgnoreCase(originalEntry.getSortString()) == 0) && (newEntry.getDisplayString().compareToIgnoreCase(originalEntry.getDisplayString()) == 0)) { resolveDisplayAmbiguity(newEntry, originalEntry); modifiedOriginalEntries.add(originalEntry); } } // Refresh the modified entry in the original set intellicutEntrySet.removeAll(modifiedOriginalEntries); intellicutEntrySet.addAll(modifiedOriginalEntries); // Now add the new entries to this set intellicutEntrySet.addAll(additionalEntrySet); } /** * Resolves an ambiguity in the displayed string between two list entities. * * This method is invoked when an entity must be added to the intellicut list, * but its display string equals that of another entity. The purpose of this * method is to modify one or both of the entities in order to resolve * this ambiguity. It is assumed that the resolution does not cause extra * ambiguities. * * If resolution is not possible, the new entry will be added to the list * entries as is, and the list will contain two entries with identical * display strings. * * Note: The ambiguity finding mechanism assumes that it is not possible for * resolution to cause further ambiguities, as these would not be detected. * * @param newEntry entry which is to be added to the list * @param existingEntry entry already existing in the list */ protected void resolveDisplayAmbiguity(IntellicutListEntry newEntry, IntellicutListEntry existingEntry) { // By default, no action is taken to resolve the ambiguity return; } /** * @return the set of all loaded list entries sorted in alphabetical order */ public SortedSet<IntellicutListEntry> getListEntrySet() { return Collections.unmodifiableSortedSet(intellicutEntrySet); } /** * @param data the data object to get a list entry for * @return the list entry that corresponds to the given data object or null if no such entry */ public IntellicutListEntry getListEntryForData(Object data) { return intellicutEntryMap.get(data); } /** * @param listEntry the list entry to get the tooltip text for * @param invoker the component to get the tooltip text for * @return the tooltip text for the list entry */ public String getToolTipTextForEntry(IntellicutListEntry listEntry, JComponent invoker) { Object data = listEntry.getData(); if (data instanceof GemEntity) { return ToolTipHelpers.getEntityToolTip(((GemEntity)data), namingPolicy, invoker); } return listEntry.getDisplayString(); } /** * Returns a set of gems that visible to the user. The visibility of a gem is determined * by several checks. First, the module containing the gem has to be visible from the * perspective. For example, the module has to be referenced by the current working module. * Second, the module has to go through the visibility predicate check to ensure its * visibility. The third check is to ensure that the given gem is a public gem, or it * is part of the working module. * @param perspective */ protected Set<GemEntity> getVisibleGemsFromPerspective(Perspective perspective) { Set<GemEntity> entities = new LinkedHashSet<GemEntity>(); List<MetaModule> modules = perspective.getVisibleMetaModules(); for (int i = 0, size = modules.size(); i < size; i++) { MetaModule module = modules.get(i); if (visibilityPolicy.isModuleVisible(module)) { // TODO currently we don't support metadata expert/hidden/preferred flag // checking for gem entities, so as long as the module is visible, // all public gems are visible entities.addAll(perspective.getVisibleGemEntities(module)); } } return entities; } /** * Returns a new IntellicutInfo if the output type of the given entry data can be connected * to the specified type expression. Returns null if the connection is not possible. * @param data the IntellicutListEntry for the new entry * @param destTypeExpr the type that the data must connect to * @param typeCheckInfo the type check info to use * @param sourceMetrics The WorkspaceSourceMetrics provider to fetch reference frequency with. * @return new IntellicutInfo or null if connection not possible */ protected static IntellicutInfo getIntellicutInfo(IntellicutListEntry data, TypeExpr destTypeExpr, TypeCheckInfo typeCheckInfo, boolean allowFreeArguments, SourceMetrics sourceMetrics) { return getIntellicutInfo(data, new TypeExpr[] { destTypeExpr }, typeCheckInfo, allowFreeArguments, sourceMetrics); } /** * Get the reference frequency for a specific list entry * @param sourceMetrics the metrics store to fetch the frequency from * @param candidate the list entry to fetch the frequency for * @return The number of times the specified gem is referred to in the body of other gems */ protected static int getReferenceFrequency(SourceMetrics sourceMetrics, IntellicutListEntry candidate) { if (sourceMetrics != null && candidate instanceof GemEntityListEntry) { GemEntityListEntry gemCandidate = (GemEntityListEntry)candidate; QualifiedName name = gemCandidate.getEntity().getName(); return sourceMetrics.getGemReferenceFrequency(name); } return 0; } /** * Returns a new IntellicutListEntry if the output type of the given entry data can be connected * to at least one of the specified type expression. Returns null if the connection is not possible. * @param outputListEntry the IntellicutListEntry * @param destTypeExprs the types available for the data to connect to * @param typeCheckInfo the type check info to use * @param allowFreeArguments whether or not the entry data is allowed to have free arguments * @param sourceMetrics The WorkspaceSourceMetrics provider to fetch reference frequency with. * @return new IntellicutListEntry or null if connection not possible */ protected static IntellicutInfo getIntellicutInfo(IntellicutListEntry outputListEntry, TypeExpr[] destTypeExprs, TypeCheckInfo typeCheckInfo, boolean allowFreeArguments, SourceMetrics sourceMetrics) { TypeExpr outputType = outputListEntry.getResultType(); int dependents = getReferenceFrequency(sourceMetrics, outputListEntry); // Code gems return null if no code is entered in the code editor. if (outputType == null || destTypeExprs == null) { return null; } ModuleTypeInfo currentModuleTypeInfo = typeCheckInfo.getModuleTypeInfo(); boolean targetIsNonParametric = !outputType.isPolymorphic(); boolean exactMatch = false; // Check whether any of the provided types are an exact match for(int typeN = 0, nTypes=destTypeExprs.length; typeN < nTypes; ++typeN) { if (targetIsNonParametric && outputType.sameType(destTypeExprs[typeN])) { exactMatch = true; break; } } // Check to see if the destTypeExpr and outputType are 'connectable'. if (!outputListEntry.isAutoBurnable()) { // Try connecting without burning. // Find the maximum closeness value for the types provided. int noBurnTypeCloseness = -1; for (int typeN = 0, nTypes = destTypeExprs.length; typeN < nTypes; ++typeN) { int closenessForType = TypeExpr.getTypeCloseness(destTypeExprs[typeN], outputType, currentModuleTypeInfo); if (closenessForType > noBurnTypeCloseness) { noBurnTypeCloseness = closenessForType; } } if (noBurnTypeCloseness != -1 && (allowFreeArguments || !outputListEntry.hasDisconnectedInputs())) { return new IntellicutInfo(AutoburnUnifyStatus.NOT_NECESSARY, -1, noBurnTypeCloseness, exactMatch, dependents); } return null; } // Try connecting with auto burning. // Find the autoburning for the type which provides the maximum closeness value. AutoburnLogic.AutoburnInfo autoburnInfo = null; int noBurnTypeCloseness = -1; for (int typeN = 0, nTypes = destTypeExprs.length; typeN < nTypes; ++typeN) { AutoburnLogic.AutoburnInfo autoburnInfoForType = AutoburnLogic.getAutoburnInfo(destTypeExprs[typeN], outputListEntry.getTypePiecesForBurning(), typeCheckInfo); noBurnTypeCloseness = Math.max(noBurnTypeCloseness, autoburnInfoForType.getNoBurnTypeCloseness()); if (autoburnInfo == null || autoburnInfoForType.getMaxTypeCloseness() > autoburnInfo.getMaxTypeCloseness()) { autoburnInfo = autoburnInfoForType; } } if (autoburnInfo != null) { AutoburnUnifyStatus autoburnUnifyStatus = autoburnInfo.getAutoburnUnifyStatus(); if (autoburnUnifyStatus != AutoburnUnifyStatus.NOT_POSSIBLE && (allowFreeArguments || canBurnAllArguments(outputListEntry, autoburnInfo))) { return new IntellicutInfo(autoburnInfo.getAutoburnUnifyStatus(), autoburnInfo.getMaxTypeCloseness(), noBurnTypeCloseness, exactMatch, dependents); } } return null; } /** * Returns a new IntellicutListEntry if any of the input types of the given list entry data * can be connected to the specified type expression. Returns null if the connection is not possible. * @param inputListEntry the IntellicutListEntry object * @param sourceTypeExpr the type that the entity must connect to * @param moduleTypeInfo the type info of the context module * @return new IntellicutListEntry or null if connection not possible */ protected static IntellicutInfo getIntellicutInfo(IntellicutListEntry inputListEntry, TypeExpr sourceTypeExpr, ModuleTypeInfo moduleTypeInfo, SourceMetrics sourceMetrics) { TypeExpr[] typePieces = inputListEntry.getArgumentTypePieces(); // No arguments means we can't connect to it. if (typePieces == null) { return null; } int references = getReferenceFrequency(sourceMetrics, inputListEntry); boolean foundCandidate = false; int finalClosenessForType = 0; boolean finalExactMatch = false; boolean sourceIsNonParametric = !sourceTypeExpr.isPolymorphic(); for (final TypeExpr typeExp : typePieces) { // Check to see if the sourceTypeExpr and one of the type pieces are 'connectable'. // We need to check the whole list, because we might encounter a "connectable but not exact" // piece before we encounter a "connectable exact match" piece. if (TypeExpr.canUnifyType(sourceTypeExpr, typeExp, moduleTypeInfo)) { foundCandidate = true; int closenessForType = TypeExpr.getTypeCloseness(sourceTypeExpr, typeExp, moduleTypeInfo); boolean exactMatch = sourceIsNonParametric && sourceTypeExpr.sameType(typeExp); if (exactMatch || closenessForType > finalClosenessForType) { finalClosenessForType = closenessForType; finalExactMatch = exactMatch || finalExactMatch; } } } if (foundCandidate) { return new IntellicutInfo(AutoburnUnifyStatus.NOT_NECESSARY, -1, finalClosenessForType, finalExactMatch, references); } else { return null; } } /** * Checks if the given autoburn info indicates that all arguments of the given list entry data can be burned. * @param listEntry the list entry * @param autoburnInfo the autoburn info * @return true if all arguments can be burned */ private static boolean canBurnAllArguments(IntellicutListEntry listEntry, AutoburnLogic.AutoburnInfo autoburnInfo) { List<BurnCombination> burnCombos = autoburnInfo.getBurnCombinations(); if (burnCombos.isEmpty()) { return false; } AutoburnLogic.BurnCombination lastBurnCombo = burnCombos.get(burnCombos.size() - 1); return lastBurnCombo.getInputsToBurn().length == listEntry.getTypePiecesForBurning().length - 1; } }