/*
* 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.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.netbeans.modules.parsing.spi.indexing.support.IndexResult;
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
import org.netbeans.modules.ruby.FindersHelper.FinderMethod;
import org.netbeans.modules.ruby.elements.IndexedElement;
import org.netbeans.modules.ruby.elements.IndexedMethod;
import org.openide.filesystems.FileObject;
/**
* Indexes Rails specific database related properties.
*
* @author Tor Norbye, Erno Mononen
*/
final class DatabasePropertiesIndexer {
private final RubyIndex index;
private final String prefix;
private final QuerySupport.Kind kind;
private final String classFqn;
private final Set<IndexedMethod> methods;
private final boolean includeDynamicFinders;
private DatabasePropertiesIndexer(RubyIndex index, String prefix,
QuerySupport.Kind kind, String classFqn, Set<IndexedMethod> methods,
boolean includeDynamicFinders) {
this.index = index;
this.prefix = prefix;
this.kind = kind;
this.classFqn = classFqn;
this.methods = methods;
this.includeDynamicFinders = includeDynamicFinders;
}
static void indexDatabaseProperties(RubyIndex index, String prefix, QuerySupport.Kind kind,
String classFqn, Set<IndexedMethod> methods, boolean includeDynamicFinders) {
DatabasePropertiesIndexer indexer = new DatabasePropertiesIndexer(index, prefix, kind, classFqn, methods, includeDynamicFinders);
indexer.addDatabaseProperties();
}
private void addDatabaseProperties() {
// Query index for database related properties
String tableName = null;
Collection<? extends IndexResult> classes = index.query(RubyIndexer.FIELD_FQN_NAME, classFqn, QuerySupport.Kind.EXACT);
for (IndexResult result : classes) {
tableName = result.getValue(RubyIndexer.FIELD_EXPLICIT_DB_TABLE);
if (tableName != null) { // just use the first found
break;
}
}
if (tableName == null) {
Inflector inflector = Inflector.getDefault();
tableName = inflector.tableize(inflector.demodulize(classFqn));
}
Collection<? extends IndexResult> result = index.query(RubyIndexer.FIELD_DB_TABLE, tableName, QuerySupport.Kind.EXACT);
List<TableDefinition> tableDefs = new ArrayList<TableDefinition>();
TableDefinition schema = null;
for (IndexResult ir : result) {
assert ir != null;
String version = ir.getValue(RubyIndexer.FIELD_DB_VERSION);
assert tableName.equals(ir.getValue(RubyIndexer.FIELD_DB_TABLE));
TableDefinition def = new TableDefinition(tableName, version, ir.getFile());
tableDefs.add(def);
String[] columns = ir.getValues(RubyIndexer.FIELD_DB_COLUMN);
if (columns != null) {
for (String column : columns) {
// TODO - do this filtering AFTER applying diffs when
// I'm doing renaming of columns etc.
def.addColumn(column);
}
}
if (RubyIndexer.SCHEMA_INDEX_VERSION.equals(version)) {
schema = def;
// With a schema I don't need to look at anything else
break;
}
}
if (tableDefs.size() > 0) {
Map<String, String> columnDefs = new HashMap<String, String>();
Map<String, FileObject> fileUrls = new HashMap<String, FileObject>();
Set<String> currentCols = new HashSet<String>();
if (schema != null) {
addColumnsFromSchema(schema, columnDefs, fileUrls, currentCols);
} else {
// Apply migration files
addColumnsFromMigrations(tableDefs, columnDefs, fileUrls, currentCols);
}
// Finally, we've "applied" the migrations - just walk
// through the datastructure and create completion matches
// as appropriate
createMethodsForColumns(tableName, columnDefs, fileUrls, currentCols);
// dynamic finders
if (includeDynamicFinders) {
createDynamicFinders(tableName, columnDefs, fileUrls, currentCols);
}
}
}
private void addColumnsFromMigrations(List<TableDefinition> tableDefs,
Map<String, String> columnDefs,
Map<String, FileObject> fileUrls,
Set<String> currentCols) {
// Apply migration files
Collections.sort(tableDefs);
for (TableDefinition def : tableDefs) {
List<String> cols = def.getColumns();
if (cols == null) {
continue;
}
for (String col : cols) {
int typeIndex = col.indexOf(';');
if (typeIndex != -1) {
String name = col.substring(0, typeIndex);
if (typeIndex < col.length() - 1 && col.charAt(typeIndex + 1) == '-') {
// Removing column
currentCols.remove(name);
} else {
currentCols.add(name);
fileUrls.put(col, def.getFileUrl());
columnDefs.put(name, col);
}
} else {
currentCols.add(col);
columnDefs.put(col, col);
fileUrls.put(col, def.getFileUrl());
}
}
}
}
private void addColumnsFromSchema(TableDefinition schema,
Map<String, String> columnDefs,
Map<String, FileObject> fileUrls,
Set<String> currentCols) {
List<String> cols = schema.getColumns();
if (cols != null) {
for (String col : cols) {
int typeIndex = col.indexOf(';');
if (typeIndex != -1) {
String name = col.substring(0, typeIndex);
if (typeIndex < col.length() - 1 && col.charAt(typeIndex + 1) == '-') {
// Removing column - this is unlikely in a
// schema.rb file!
currentCols.remove(col);
} else {
currentCols.add(name);
fileUrls.put(col, schema.getFileUrl());
columnDefs.put(name, col);
}
} else {
currentCols.add(col);
columnDefs.put(col, col);
fileUrls.put(col, schema.getFileUrl());
}
}
}
}
private void createMethodsForColumns(String tableName,
Map<String, String> columnDefs,
Map<String, FileObject> fileUrls,
Set<String> currentCols) {
for (String column : currentCols) {
if (column.startsWith(prefix)) {
if (kind == QuerySupport.Kind.EXACT) {
// Ensure that the method is not longer than the prefix
if ((column.length() > prefix.length())) {
continue;
}
} else {
// REGEXP, CAMELCASE filtering etc. not supported here
assert (kind == QuerySupport.Kind.PREFIX) ||
(kind == QuerySupport.Kind.CASE_INSENSITIVE_PREFIX);
}
String c = columnDefs.get(column);
String type = tableName;
int semicolonIndex = c.indexOf(';');
if (semicolonIndex != -1) {
type = c.substring(semicolonIndex + 1);
}
FileObject fileUrl = fileUrls.get(column);
String signature = column;
String fqn = tableName + "#" + column;
String clz = type;
String require = null;
String attributes = "";
int flags = 0;
IndexedMethod method =
IndexedMethod.create(index, signature, fqn, clz, fileUrl, require, attributes, flags, index.getContext());
method.setMethodType(IndexedMethod.MethodType.DBCOLUMN);
method.setType(RailsMigrationTypeMapper.getMappedType(type));
method.setSmart(true);
methods.add(method);
}
}
}
private void createDynamicFinders(String tableName,
Map<String, String> columnDefs,
Map<String, FileObject> fileUrls,
Set<String> currentCols) {
if (kind == QuerySupport.Kind.EXACT) {
return;
}
List<FinderMethod> finders = new ArrayList<FinderMethod>(FindersHelper.getFinderSignatures(prefix, currentCols));
for (FindersHelper.FinderMethod finder : finders) {
String methodName = finder.getName();
String methodSignature = finder.getSignature();
if (!methodName.startsWith(prefix) || prefix.length() > methodName.length()) {
continue;
}
// XXX: what's this needed for?
String column = finder.getColumn();
FileObject fileUrl = fileUrls.get(column);
String clz = classFqn;
String require = null;
int flags = IndexedElement.STATIC;
String attributes = IndexedElement.flagToString(flags) + ";;;" + "options(:first|:all),args(=>conditions|order|group|limit|offset|joins|readonly:bool|include|select|from|readonly:bool|lock:bool)";
String fqn = tableName + "#" + methodSignature;
IndexedMethod method =
IndexedMethod.create(index, methodSignature, fqn, clz, fileUrl, require, attributes, flags, index.getContext());
method.setMethodType(IndexedMethod.MethodType.DYNAMIC_FINDER);
method.setInherited(false);
method.setSmart(true);
methods.add(method);
}
}
private static class TableDefinition implements Comparable<TableDefinition> {
private String version;
/** table is redundant, I only search by exact tablenames anyway */
private String table;
private FileObject fileUrl;
private List<String> cols;
TableDefinition(String table, String version, FileObject fileUrl) {
this.table = table;
this.version = version;
this.fileUrl = fileUrl;
}
public int compareTo(TableDefinition o) {
// See if we're comparing an old style (3-digit) version number with a new Rails 2.1 UTC version
if (version.length() != o.version.length()) {
return version.length() - o.version.length();
}
// I can do string comparisons here because the strings
// are all padded with zeroes on the left (so 100 is going
// to be greater than 099, which wouldn't be true for "99".)
return version.compareTo(o.version);
}
FileObject getFileUrl() {
return fileUrl;
}
void addColumn(String column) {
if (cols == null) {
cols = new ArrayList<String>();
}
cols.add(column);
}
List<String> getColumns() {
return cols;
}
}
}