/**
* Copyright (c) 2011 Cloudsmith Inc. and other contributors, as listed below.
* 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:
* Cloudsmith
*
*/
package org.cloudsmith.geppetto.pp.dsl.contentassist;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Set;
import org.apache.commons.codec.language.DoubleMetaphone;
import org.apache.commons.lang.StringUtils;
import org.cloudsmith.geppetto.common.score.ScoreKeeper;
import org.cloudsmith.geppetto.common.score.ScoreKeeper.ScoreEntry;
import org.cloudsmith.geppetto.pp.PPPackage;
import org.cloudsmith.geppetto.pp.dsl.PPDSLConstants;
import org.cloudsmith.geppetto.pp.dsl.linking.PPSearchPath;
import org.cloudsmith.geppetto.pp.pptp.PPTPPackage;
import org.eclipse.emf.ecore.EClass;
import org.eclipse.xtext.naming.IQualifiedNameConverter;
import org.eclipse.xtext.naming.QualifiedName;
import org.eclipse.xtext.resource.IEObjectDescription;
import com.google.common.collect.Lists;
import com.google.common.collect.Sets;
import com.google.inject.Inject;
/**
* Generator of proposals
*
*/
public class PPProposalsGenerator {
/**
* compares the pronunciation difference between given reference and candidates
*
*/
public static class PronunciationComparator implements Comparator<String> {
private DoubleMetaphone encoder;
private String metaphoneName;
PronunciationComparator(DoubleMetaphone encoder, String metaphoneReference) {
this.encoder = encoder;
this.metaphoneName = metaphoneReference;
}
@Override
public int compare(String a, String b) {
String am = encoder.encode(a);
String bm = encoder.encode(b);
int al = StringUtils.getLevenshteinDistance(metaphoneName, am);
int bl = StringUtils.getLevenshteinDistance(metaphoneName, bm);
if(al == bl)
return 0;
return al < bl
? -1
: 1;
}
}
/**
* PP FQN to/from Xtext QualifiedName converter.
*/
@Inject
IQualifiedNameConverter converter;
protected final static EClass[] DEF_AND_TYPE_ARGUMENTS = {
PPPackage.Literals.DEFINITION_ARGUMENT, PPTPPackage.Literals.TYPE_ARGUMENT };
protected final static EClass[] DEF_AND_TYPE = { PPTPPackage.Literals.TYPE, PPPackage.Literals.DEFINITION };
/**
* Computes attribute proposals where the class/definition name must match exactly, but where
* parameters are processed with fuzzy logic.
*
* @param currentName
* @param descs
* @param searchPath
* TODO
* @param types
* @return
*/
public String[] computeAttributeProposals(final QualifiedName currentName, Collection<IEObjectDescription> descs,
PPSearchPath searchPath) {
if(currentName.getSegmentCount() < 2)
return new String[0];
final DoubleMetaphone encoder = new DoubleMetaphone();
final String metaphoneName = encoder.encode(currentName.getLastSegment());
Collection<String> proposals = generateAttributeCandidates(currentName, descs, searchPath);
// propose all, but sort them based on likeness
String[] result = new String[proposals.size()];
proposals.toArray(result);
Arrays.sort(result, new PronunciationComparator(encoder, metaphoneName));
return result;
}
public String[] computeDistinctProposals(String currentName, List<IEObjectDescription> descs) {
return computeDistinctProposals(currentName, descs, false);
}
/**
* Attempts to produce a list of more distinct names than the given name by making
* name absolute.
*
* @param currentName
* the name for which proposals are wanted
* @param descs
* index of descriptors
*/
public String[] computeDistinctProposals(String currentName, List<IEObjectDescription> descs,
boolean upperCaseProposals) {
List<String> proposals = Lists.newArrayList();
if(currentName.startsWith("::"))
return new String[0]; // can not make a global name more specific than what it already is
for(IEObjectDescription d : descs) {
String s = converter.toString(d.getQualifiedName());
if(!s.startsWith("::")) {
String s2 = "::" + s;
if(!(s2.equals(currentName) || proposals.contains(s2)))
proposals.add(s2);
}
if(s.equals(currentName) || proposals.contains(s))
continue;
proposals.add(s);
}
String[] props = proposals.toArray(new String[proposals.size()]);
Arrays.sort(props);
return upperCaseProposals
? toUpperCaseProposals(props)
: props;
}
/**
* Attempts to produce a list of names that are close to the given name. At most 5 proposals
* are generated. The returned proposals are made in order of "pronunciation distance" which is
* obtained by taking the Levenshtein distance between the Double Monophone encodings of
* candidate and given name. Candidates are selected as the names with shortest Levenshtein distance
* and names that are Monophonically equal, or starts or ends monophonically.
*
* @param currentName
* the name for which proposals are to be generated
* @param descs
* the descriptors of available named values
* @param searchPath
* TODO
* @param types
* if stated, the wanted types of named values
* @return
* array of proposals, possibly empty, but never null.
*/
public String[] computeProposals(final String currentName, Collection<IEObjectDescription> descs,
boolean upperCaseProposals, PPSearchPath searchPath, EClass... types) {
if(currentName == null || currentName.length() < 1)
return new String[0];
// compute the 5 best matches and only accept if score <= 5
ScoreKeeper<IEObjectDescription> tracker = new ScoreKeeper<IEObjectDescription>(5, false, 5);
// List<IEObjectDescription> metaphoneAlike = Lists.newArrayList();
final DoubleMetaphone encoder = new DoubleMetaphone();
final String metaphoneName = encoder.encode(currentName);
for(IEObjectDescription d : descs) {
EClass c = d.getEClass();
typeok: if(types != null && types.length > 0) {
for(EClass wanted : types)
if((wanted == c || wanted.isSuperTypeOf(c)))
break typeok;
continue;
}
// filter based on path visibility
if(searchPath.searchIndexOf(d) == -1)
continue; // not visible according to path
String candidateName = converter.toString(d.getName());
tracker.addScore(StringUtils.getLevenshteinDistance(currentName, candidateName), d);
String candidateMetaphone = encoder.encode(candidateName);
// metaphone matches are scored on the pronounciation distance
if(metaphoneName.equals(candidateMetaphone) //
||
candidateMetaphone.startsWith(metaphoneName) //
|| candidateMetaphone.endsWith(metaphoneName) //
)
tracker.addScore(StringUtils.getLevenshteinDistance(metaphoneName, candidateMetaphone), d);
// System.err.printf("Metaphone alike: %s == %s\n", currentName, candidateName);
}
List<String> result = Lists.newArrayList();
// System.err.print("Scores = ");
for(ScoreEntry<IEObjectDescription> entry : tracker.getScoreEntries()) {
String s = converter.toString(entry.getData().getName());
result.add(s);
// System.err.printf("%d %s, ", entry.getScore(), s);
}
// System.err.println();
String[] proposals = result.toArray(new String[result.size()]);
PronunciationComparator x = new PronunciationComparator(encoder, metaphoneName);
Arrays.sort(proposals, x);
// System.err.print("Order = ");
// for(int i = 0; i < proposals.length; i++)
// System.err.printf("%s, ", proposals[i]);
// System.err.println();
return upperCaseProposals
? toUpperCaseProposals(proposals)
: proposals;
}
public String[] computeProposals(final String currentName, Collection<IEObjectDescription> descs,
PPSearchPath searchPath, EClass... types) {
return computeProposals(currentName, descs, false, searchPath, types);
}
public Collection<String> generateAttributeCandidates(final QualifiedName currentName,
Collection<IEObjectDescription> descs, PPSearchPath searchPath) {
// find candidate names
if(currentName.getSegmentCount() < 2)
return Collections.emptySet();
// unique set of proposed attribute names (last segment)
Set<String> proposed = Sets.newHashSet();
List<QualifiedName> classesToSearch = Lists.newArrayList();
Set<QualifiedName> visited = Sets.newHashSet();
classesToSearch.add(currentName.skipLast(1));
while(classesToSearch.size() > 0) {
QualifiedName prefix = classesToSearch.remove(0);
if(visited.contains(prefix))
continue;
visited.add(prefix); // prevent recursion
// find all that start with className and are properties or parameters
// also find the class/definition itself (possibly ambiguous).
for(IEObjectDescription d : descs) {
if(searchPath.searchIndexOf(d) == -1)
continue; // not visible
EClass ec = d.getEClass();
QualifiedName name = d.getName();
if(name.startsWith(prefix)) {
if(name.getSegmentCount() == prefix.getSegmentCount()) {
// exact match, check if this is the correct type
if(DEF_AND_TYPE[0].isSuperTypeOf(ec) || DEF_AND_TYPE[1].isSuperTypeOf(ec)) {
String parentName = d.getUserData(PPDSLConstants.PARENT_NAME_DATA);
if(parentName != null && parentName.length() > 0)
classesToSearch.add(converter.toQualifiedName(parentName));
}
continue; // exact match can not be an argument
}
if(DEF_AND_TYPE_ARGUMENTS[0].isSuperTypeOf(ec) || DEF_AND_TYPE_ARGUMENTS[1].isSuperTypeOf(ec))
proposed.add(d.getName().getLastSegment());
}
}
}
return proposed;
}
private String toInitialUpperCase(String s) {
if(s == null || s.length() < 1)
return s;
char c = s.charAt(0);
if(Character.isUpperCase(c))
return s;
return Character.toString(c).toUpperCase() + s.substring(1);
}
private String toUpperCaseProposal(String original) {
QualifiedName fqn = converter.toQualifiedName(original);
String[] segments = new String[fqn.getSegmentCount()];
for(int i = 0; i < fqn.getSegmentCount(); i++)
segments[i] = toInitialUpperCase(fqn.getSegment(i));
return converter.toString(QualifiedName.create(segments));
}
private String[] toUpperCaseProposals(String[] original) {
for(int i = 0; i < original.length; i++) {
original[i] = toUpperCaseProposal(original[i]);
}
return original;
};
}