/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* The contents of this file are subject to the terms of either the GNU
* General Public License Version 2 only ("GPL") or the Common
* Development and Distribution License("CDDL") (collectively, the
* "License"). You may not use this file except in compliance with the
* License. You can obtain a copy of the License at
* http://www.netbeans.org/cddl-gplv2.html
* or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
* specific language governing permissions and limitations under the
* License. When distributing the software, include this License Header
* Notice in each file and include the License file at
* nbbuild/licenses/CDDL-GPL-2-CP. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle in the GPL Version 2 section of the License file that
* accompanied this code. If applicable, add the following below the
* License Header, with the fields enclosed by brackets [] replaced by
* your own identifying information:
* "Portions Copyrighted [year] [name of copyright owner]"
*
* If you wish your version of this file to be governed by only the CDDL
* or only the GPL Version 2, indicate your decision by adding
* "[Contributor] elects to include this software in this distribution
* under the [CDDL or GPL Version 2] license." If you do not indicate a
* single choice of license, a recipient has the option to distribute
* your version of this file under either the CDDL, the GPL Version 2 or
* to extend the choice of license to its licensees as provided above.
* However, if you add GPL Version 2 code and therefore, elected the GPL
* Version 2 license, then the option applies only if the new code is
* made subject to such option by the copyright holder.
*
* Contributor(s):
*
* Portions Copyrighted 2009 Sun Microsystems, Inc.
*/
package org.netbeans.modules.ruby;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.jrubyparser.ast.Node;
import org.jrubyparser.ast.NodeType;
import org.jrubyparser.ast.SymbolNode;
/**
* A helper class for generating signatures for various ActiveRecord
* dynamic finder methods.
*
* @author Erno Mononen
*/
final class FindersHelper {
private static final String ATTRIBUTE_SEPARATOR_BASE = "_and"; //NOI18N
private static final String ATTRIBUTE_SEPARATOR = ATTRIBUTE_SEPARATOR_BASE + "_"; //NOI18N
/**
* The standard find method in AR:Base.
*/
private static final String FIND ="find"; //NOI18N
/**
* Alias for find :all in AR:Base.
*/
private static final String ALL ="all"; //NOI18N
/**
* The names of the default find methods.
*/
private static final String[] STANDARD_FINDERS = {ALL, FIND}; //NOI18N
/**
* The threshold for the column count after which we will compute only one
* level of finders.
*/
private static final int THRESHOLD = 40;
/**
* The max amount of dynamic finders to compute.
*/
private static final int MAX_ITEMS = (int) Math.pow(THRESHOLD, 2);
private enum FinderType {
FIND_BY("find_by_") {
boolean isMultiple() { return false; }
},
FIND_ALL_BY("find_all_by_") {
boolean isMultiple() { return true;}
},
FIND_LAST_BY("find_last_by_") {
boolean isMultiple() { return false; }
},
SCOPED_BY("scoped_by_") {
boolean isMultiple() {return true;}
@Override boolean hasOptions() { return false;}
};
private final String prefix;
FinderType(String prefix) {this.prefix = prefix; }
abstract boolean isMultiple();
String getPrefix() { return prefix; }
boolean hasOptions() { return true; }
}
/**
* Common finder method prefixes.
*/
private final Collection<FinderType> prefixes;
/**
* All columns.
*/
private final Collection<String> columns;
/**
* Columns already present in the prefix.
*/
private final String[] existingColumns;
/**
* The max depth of finders to compute, i.e. the how many columns to combine.
*/
private final int maxDepth;
private FindersHelper(Collection<FinderType> prefixes, List<String> existingColumns, Collection<String> columns) {
this.prefixes = prefixes;
this.columns = columns;
this.existingColumns = existingColumns.toArray(new String[existingColumns.size()]);
int size = columns.size();
// the depth, needs to be limited since the number of
// possible combinations grows exponentially
// by default compute one level ahead (approriate for code completion)
this.maxDepth = size > THRESHOLD ? 0 : 1;
}
// package private for unit tests
static List<String> extractColumns(String method) {
for (FinderType finder : FinderType.values()) {
String prefix = finder.getPrefix();
int prefixIdx = method.indexOf(prefix);
if (prefixIdx != -1) {
String woPrefix = method.substring(prefix.length());
if (woPrefix.isEmpty()) {
return Collections.<String>emptyList();
}
if (woPrefix.endsWith(ATTRIBUTE_SEPARATOR_BASE)) {
woPrefix = woPrefix.substring(0, woPrefix.length() - ATTRIBUTE_SEPARATOR_BASE.length());
}
return Arrays.asList(woPrefix.split(ATTRIBUTE_SEPARATOR));
}
}
return Collections.<String>emptyList();
}
/**
* Gets all the possible finder attribute combinations for the given
* <code>prefix</code> and <code>columns</code>,
* e.g. for columns 'name' and 'price' this would return "name",
* "name_and_price", "price", "price_and_name".
*
* @param columns
* @param prefix the prefix of the method.
* @return
*/
static List<FinderMethod> getFinderSignatures(String prefix, Collection<String> columns) {
Set<String> columnsCopy = new HashSet<String>(columns);
List<String> existingColumns = extractColumns(prefix);
if (!existingColumns.isEmpty()) {
columnsCopy.removeAll(existingColumns);
}
FindersHelper helper = new FindersHelper(matchingFinderPrefixes(prefix), existingColumns, columnsCopy);
return helper.computeSignatures();
}
private static List<FinderType> matchingFinderPrefixes(String methodPrefix) {
List<FinderType> result = new ArrayList<FinderType>(5);
for (FinderType finder : FinderType.values()) {
String finderPrefix = finder.getPrefix();
if (methodPrefix.length() >= finderPrefix.length()) {
if (finderPrefix.startsWith(methodPrefix.substring(0, finderPrefix.length()))) {
result.add(finder);
}
} else if (finderPrefix.startsWith(methodPrefix)) {
result.add(finder);
}
}
return result;
}
private List<FinderMethod> computeSignatures() {
Set<String> combinations = new HashSet<String>();
String rootPrefix = concatenate(existingColumns);
for (String baseColumn : columns) {
String root = rootPrefix.isEmpty()
? baseColumn
: concatenate(rootPrefix, baseColumn);
combinations.add(root);
Set<String> copy = new HashSet<String>(columns);
copy.remove(baseColumn);
addCombinations(root, copy, combinations, 0);
}
List<FinderMethod> result = new ArrayList<FinderMethod>(combinations.size());
for (FinderType prefix : prefixes) {
for (Iterator<String> it = combinations.iterator(); it.hasNext();) {
result.add(new FinderMethod(prefix, it.next()));
}
}
Collections.sort(result);
return result;
}
private void addCombinations(String root, Set<String> others, Set<String> result, int depth) {
// give up, too many columns
if (depth >= maxDepth || result.size() >= MAX_ITEMS) {
return;
}
for (String o : others) {
String base = concatenate(root, o);
result.add(base);
Set<String> rest = new HashSet<String>(others);
rest.remove(o);
addCombinations(base, rest, result, depth + 1);
}
}
private String concatenate(String... columns) {
StringBuilder result = new StringBuilder();
for (int i = 0; i < columns.length; i++) {
String col = columns[i];
result.append(col);
if (i + 1 < columns.length) {
result.append(ATTRIBUTE_SEPARATOR);
}
}
return result.toString();
}
static int nextAttributeLocation(String finderMethodName, int fromIndex) {
return finderMethodName.indexOf(ATTRIBUTE_SEPARATOR, fromIndex);
}
static String subToNextAttribute(String finderMethodName, int attributeSeparatorIndex) {
return finderMethodName.substring(0, attributeSeparatorIndex + ATTRIBUTE_SEPARATOR.length() - 1);
}
static boolean isFinderMethod(String name) {
return isFinderMethod(name, true);
}
static boolean isFinderMethod(String name, boolean includeStandardFinders) {
for (FinderType each : FinderType.values()) {
if (name.startsWith(each.getPrefix())) {
return true;
}
}
if (includeStandardFinders) {
for (String each : STANDARD_FINDERS) {
if (name.equals(each)) {
return true;
}
}
}
return false;
}
/**
* Look up the right return type for the given finder call.
*/
static RubyType pickFinderType(final Node call, final String method, final RubyType model) {
boolean multiple = false;
boolean foundMatching = false;
for (FinderType finder : FinderType.values()) {
if (method.startsWith(finder.getPrefix())) {
foundMatching = true;
multiple = finder.isMultiple();
break;
}
}
// regular "find" (which is not a dynamic finder)
if (!foundMatching && method.equals(FIND)) { // NOI18N
// Finder method that does both - gotta inspect it
List<Node> nodes = new ArrayList<Node>();
AstUtilities.addNodesByType(call, new NodeType[]{NodeType.SYMBOLNODE}, nodes);
boolean foundAll = false;
for (Node n : nodes) {
SymbolNode symbol = (SymbolNode) n;
if ("all".equals(symbol.getName())) { // NOI18N
foundAll = true;
break;
}
}
multiple = foundAll;
foundMatching = true;
} else if (!foundMatching && method.equals(ALL)) {
multiple = true;
foundMatching = true;
} else if (!foundMatching) {
// Not sure - probably some other locally defined finder method;
// just default to the model name
multiple = false;
}
if (multiple) {
return RubyType.ARRAY;
} else {
return model;
}
}
static class FinderMethod implements Comparable<FinderMethod> {
private final FinderType finder;
private final String attributes;
public FinderMethod(FinderType prefix, String attributes) {
this.finder = prefix;
this.attributes = attributes;
}
public String getName() {
return finder.getPrefix() + attributes;
}
public String getSignature() {
StringBuilder result = new StringBuilder(finder.getPrefix() + attributes + "(");
String[] params = attributes.split(ATTRIBUTE_SEPARATOR);
for (int i = 0; i < params.length; i++) {
String param = params[i];
result.append(param);
if (i < params.length - 1) {
result.append(", ");
}
}
if (finder.hasOptions()) {
result.append(", *options");
}
result.append(")");
return result.toString();
}
public String getColumn() {
// the primary column
int andIndex = attributes.indexOf(ATTRIBUTE_SEPARATOR);
if (andIndex == -1) {
return attributes;
}
return attributes.substring(0, andIndex);
}
public int compareTo(FinderMethod o) {
return getName().compareTo(o.getName());
}
@Override
public String toString() {
return FinderMethod.class.getSimpleName() + "[name: " + getName() + "]";
}
}
}