com.puppetlabs.geppetto.pp.dsl.contentassist.PPProposalsGenerator.java Source code

Java tutorial

Introduction

Here is the source code for com.puppetlabs.geppetto.pp.dsl.contentassist.PPProposalsGenerator.java

Source

/**
 * Copyright (c) 2013 Puppet Labs, 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:
 *   Puppet Labs
 */
package com.puppetlabs.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 com.puppetlabs.geppetto.common.score.ScoreKeeper;
import com.puppetlabs.geppetto.common.score.ScoreKeeper.ScoreEntry;
import com.puppetlabs.geppetto.pp.PPPackage;
import com.puppetlabs.geppetto.pp.dsl.PPDSLConstants;
import com.puppetlabs.geppetto.pp.dsl.linking.PPSearchPath;
import com.puppetlabs.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;
    };

}