blob: f24b7d5482a65c855dff1a0b899735d0f4b2dc2e [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.netbeans.modules.javascript2.model.api;
import java.io.IOException;
import java.lang.ref.SoftReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.WeakHashMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.netbeans.modules.csl.api.Modifier;
import org.netbeans.modules.csl.api.OffsetRange;
import org.netbeans.modules.javascript2.model.spi.IndexChangeSupport;
import org.netbeans.modules.javascript2.model.spi.QuerySupportFactory;
import org.netbeans.modules.javascript2.types.api.TypeUsage;
import org.netbeans.modules.parsing.spi.indexing.support.IndexResult;
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
import org.openide.filesystems.FileObject;
import org.openide.util.Lookup;
/**
*
* @author Petr Pisl
*/
public final class Index {
public static final String FIELD_BASE_NAME = "bn"; //NOI18N
/**
* The same as FIELD_BASE_NAME, but case insensitive.
*/
public static final String FIELD_BASE_NAME_INSENSITIVE = "bni"; // NOI18N
/**
* In this field is in the lucene also coded, whether the object is anonymous (last char is 'A')
* or normal object (last char is 'O'). If someone needs to access this field
* directly, then has to be count with this.
*/
public static final String FIELD_FQ_NAME = "fqn"; //NOI18N
public static final String FIELD_OFFSET = "offset"; //NOI18N
public static final String FIELD_ASSIGNMENTS = "assign"; //NOI18N
public static final String FIELD_RETURN_TYPES = "return"; //NOI18N
public static final String FIELD_PARAMETERS = "param"; //NOI18N
public static final String FIELD_FLAG = "flag"; //NOI18N
public static final String FIELD_ARRAY_TYPES = "array"; //NOI18N
public static final String FIELD_USAGE = "usage"; //NOI18N
private static final String PROPERTIES_PATTERN = "\\.[^\\.]*[^" + IndexedElement.PARAMETER_POSTFIX + "]";
@org.netbeans.api.annotations.common.SuppressWarnings("MS_MUTABLE_ARRAY")
public static final String[] TERMS_BASIC_INFO = new String[] { FIELD_BASE_NAME, FIELD_FQ_NAME, FIELD_OFFSET,
FIELD_RETURN_TYPES, FIELD_PARAMETERS, FIELD_FLAG, FIELD_ASSIGNMENTS, FIELD_ARRAY_TYPES};
private static final Logger LOG = Logger.getLogger(Index.class.getName());
private static final ReentrantReadWriteLock LOCK = new ReentrantReadWriteLock();
private static final Lock READ_LOCK = LOCK.readLock();
private static final Lock WRITE_LOCK = LOCK.writeLock();
private static final WeakHashMap<FileObject, Index> INDEX_CACHE = new WeakHashMap<FileObject, Index>();
// empirical values (update if index is changed)
private static final int MAX_ENTRIES_CACHE_INDEX_RESULT = 2000;
private static final int MAX_CACHE_VALUE_SIZE = 1000000;
private static final int AVERAGE_BASIC_INFO_SIZE = 60;
// cache to keep latest index results. The cache is cleaned if a file is saved
// or a file has to be reindexed due to an external change
/* GuardedBy(LOCK) */
private static final Map<CacheKey, SoftReference<CacheValue>> CACHE_INDEX_RESULT_SMALL = new LinkedHashMap<CacheKey, SoftReference<CacheValue>>(
MAX_ENTRIES_CACHE_INDEX_RESULT + 1, 0.75F, true) {
@Override
public boolean removeEldestEntry(Map.Entry eldest) {
return size() > MAX_ENTRIES_CACHE_INDEX_RESULT;
}
};
/* GuardedBy(LOCK) */
private static final Map<CacheKey, SoftReference<CacheValue>> CACHE_INDEX_RESULT_LARGE = new LinkedHashMap<CacheKey, SoftReference<CacheValue>>(
(MAX_ENTRIES_CACHE_INDEX_RESULT / 4) + 1, 0.75F, true) {
@Override
public boolean removeEldestEntry(Map.Entry eldest) {
return size() > (MAX_ENTRIES_CACHE_INDEX_RESULT / 4);
}
};
private static final Map<StatsKey, StatsValue> QUERY_STATS = new HashMap<StatsKey, StatsValue>();
private static final ChangeListener INVALIDATE_LISTENER = new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
WRITE_LOCK.lock();
try {
CACHE_INDEX_RESULT_SMALL.clear();
CACHE_INDEX_RESULT_LARGE.clear();
INDEX_CACHE.clear();
LOG.log(Level.FINEST, "Cache cleared");
} finally {
WRITE_LOCK.unlock();
}
}
};
static {
// FIXME listen for lookup changes ?
IndexChangeSupport changeSupport = Lookup.getDefault().lookup(IndexChangeSupport.class);
if (changeSupport != null) {
changeSupport.addChangeListener(INVALIDATE_LISTENER);
}
}
/* GuardedBy(QUERY_STATS) */
private static int cacheHit;
/* GuardedBy(QUERY_STATS) */
private static int cacheMiss;
private final QuerySupport querySupport;
private final boolean updateCache;
private Index(QuerySupport querySupport, boolean updateCache) {
this.querySupport = querySupport;
this.updateCache = updateCache;
}
public static Index get(Collection<FileObject> roots) {
// XXX no cache - is it needed?
LOG.log(Level.FINE, "JsIndex for roots: {0}", roots); //NOI18N
QuerySupportFactory f = Lookup.getDefault().lookup(QuerySupportFactory.class);
return new Index(f != null ? f.get(roots) : null, false);
}
public static Index get(FileObject fo) {
Index index = INDEX_CACHE.get(fo);
if (index == null) {
LOG.log(Level.FINE, "Creating JsIndex for FileObject: {0}", fo); //NOI18N
QuerySupportFactory f = Lookup.getDefault().lookup(QuerySupportFactory.class);
index = new Index(f != null
? f.get(QuerySupport.findRoots(fo, null, null, Collections.<String>emptySet()))
: null, true);
INDEX_CACHE.put(fo, index);
}
return index;
}
public Collection<? extends IndexResult> query(final String fieldName, final String fieldValue,
final QuerySupport.Kind kind, final String... fieldsToLoad) {
if (querySupport == null) {
return Collections.<IndexResult>emptySet();
}
try {
CacheKey key = new CacheKey(this, fieldName, fieldValue, kind);
CacheValue value = getCachedValue(key, fieldsToLoad);
if (value != null) {
logStats(value.getResult(), true, fieldsToLoad);
return value.getResult();
}
Collection<? extends IndexResult> result = querySupport.query(
fieldName, fieldValue, kind, fieldsToLoad);
if (updateCache) {
WRITE_LOCK.lock();
try {
value = getCachedValue(key, fieldsToLoad);
if (value != null) {
logStats(value.getResult(), false, fieldsToLoad);
return value.getResult();
}
value = new CacheValue(fieldsToLoad, result);
if ((result.size() * AVERAGE_BASIC_INFO_SIZE) < MAX_CACHE_VALUE_SIZE) {
CACHE_INDEX_RESULT_SMALL.put(key, new SoftReference<>(value));
} else {
CACHE_INDEX_RESULT_LARGE.put(key, new SoftReference<>(value));
}
logStats(result, false, fieldsToLoad);
return value.getResult();
} finally {
WRITE_LOCK.unlock();
}
}
logStats(result, false, fieldsToLoad);
return result;
} catch (IOException ioe) {
LOG.log(Level.WARNING, null, ioe);
}
return Collections.<IndexResult>emptySet();
}
public Collection<IndexedElement> getGlobalVar(String prefix) {
prefix = prefix == null ? "" : prefix; //NOI18N
ArrayList<IndexedElement> globals = new ArrayList<IndexedElement>();
long start = System.currentTimeMillis();
String indexPrefix = escapeRegExp(prefix) + "[^\\.]*[" + IndexedElement.OBJECT_POSFIX + "]"; //NOI18N
Collection<? extends IndexResult> globalObjects = query(Index.FIELD_FQ_NAME, indexPrefix, QuerySupport.Kind.REGEXP, TERMS_BASIC_INFO); //NOI18N
for (IndexResult indexResult : globalObjects) {
IndexedElement indexedElement = IndexedElement.create(indexResult);
globals.add(indexedElement);
}
long end = System.currentTimeMillis();
LOG.log(Level.FINE, "Obtaining globals from the index took: {0}", (end - start)); //NOI18N
return globals;
}
private static CacheValue getCachedValue(CacheKey key, String... fieldsToLoad) {
READ_LOCK.lock();
try {
CacheValue value = null;
SoftReference<CacheValue> currentReference = CACHE_INDEX_RESULT_SMALL.get(key);
if (currentReference != null) {
value = currentReference.get();
}
if (value == null || !value.contains(fieldsToLoad)) {
currentReference = CACHE_INDEX_RESULT_LARGE.get(key);
if (currentReference != null) {
value = currentReference.get();
}
if (value == null || !value.contains(fieldsToLoad)) {
return null;
} else {
return value;
}
} else {
return value;
}
} finally {
READ_LOCK.unlock();
}
}
private static void logStats(Collection<? extends IndexResult> result, boolean hit, String... fieldsToLoad) {
if (!LOG.isLoggable(Level.FINEST)) {
return;
}
int size = 0;
for (String field : fieldsToLoad) {
for (IndexResult r : result) {
String val = r.getValue(field);
size += val == null ? 0 : val.length();
}
}
synchronized (QUERY_STATS) {
if (hit) {
cacheHit++;
} else {
cacheMiss++;
}
StatsKey statsKey = new StatsKey(fieldsToLoad);
StatsValue statsValue = QUERY_STATS.get(statsKey);
if (statsValue == null) {
QUERY_STATS.put(statsKey,
new StatsValue(1, result.size(), size));
} else {
QUERY_STATS.put(statsKey,
new StatsValue(statsValue.getRequests() + 1,
statsValue.getCount() + result.size(), statsValue.getSize() + size));
}
if ((cacheHit + cacheMiss) % 500 == 0) {
LOG.log(Level.FINEST, "Cache hit: " + cacheHit + ", Cache miss: "
+ cacheMiss + ", Ratio: " + (cacheHit / cacheMiss));
for (Map.Entry<StatsKey, StatsValue> entry : QUERY_STATS.entrySet()) {
LOG.log(Level.FINEST, entry.getKey() + ": " + entry.getValue());
}
}
}
}
private static Collection<IndexedElement> getElementsByPrefix(String prefix, Collection<IndexedElement> items) {
Collection<IndexedElement> result = new ArrayList<IndexedElement>();
for (IndexedElement indexedElement : items) {
if (indexedElement.getName().startsWith(prefix)) {
result.add(indexedElement);
}
}
return result;
}
public Collection <IndexedElement> getPropertiesWithPrefix(String fqn, String prexif) {
return getElementsByPrefix(prexif, getProperties(fqn));
}
public Collection <IndexedElement> getProperties(String fqn) {
return getProperties(fqn, 0, new ArrayList<String>());
}
public Collection <IndexedElement> getUsagesFromExpression(final List<String> expChain) {
if (expChain == null || expChain.isEmpty()) {
return Collections.emptyList();
}
String searchText = expChain.get(0) + ':';
Collection<? extends IndexResult> results = query(Index.FIELD_USAGE, searchText, QuerySupport.Kind.PREFIX, Index.FIELD_USAGE); //NOI18N
ArrayList<IndexedElement> usages = new ArrayList<IndexedElement>();
Set<String> alreadyUsed = new HashSet<String>();
for (IndexResult indexResult : results) {
String[] fields = indexResult.getValues(Index.FIELD_USAGE);
FileObject fo = indexResult.getFile();
for (String field : fields) {
if (field.startsWith(searchText)) {
String[] parts = field.split(":");
for (String property : parts) {
String[] split = property.split("#");
if(split.length == 2 && !alreadyUsed.contains(split[0])) {
alreadyUsed.add(split[0]);
IndexedElement element = new IndexedElement(fo, split[0], split[0], false, false, split[1].equals("F") ? JsElement.Kind.FUNCTION : JsElement.Kind.OBJECT,
OffsetRange.NONE, Collections.singleton(Modifier.PUBLIC), Collections.emptyList(), false);
usages.add(element);
}
}
}
}
}
return usages;
}
private final int MAX_FIND_PROPERTIES_RECURSION = 15;
private Collection <IndexedElement> getProperties(String fqn, int deepLevel, Collection<String> resolvedTypes) {
if (deepLevel > MAX_FIND_PROPERTIES_RECURSION) {
return Collections.emptyList();
}
Collection<IndexedElement> result = new ArrayList<IndexedElement>();
if (!resolvedTypes.contains(fqn)) {
resolvedTypes.add(fqn);
deepLevel = deepLevel + 1;
Collection<? extends IndexResult> results = findByFqn(fqn, Index.FIELD_ASSIGNMENTS);
for (IndexResult indexResult : results) {
// find assignment to for the fqn
Collection<TypeUsage> assignments = IndexedElement.getAssignments(indexResult);
if (!assignments.isEmpty()) {
TypeUsage type = assignments.iterator().next();
if (!resolvedTypes.contains(type.getType())) {
result.addAll(getProperties(type.getType(), deepLevel, resolvedTypes));
}
}
}
// find properties of the fqn
// String pattern = escapeRegExp(fqn) + PROPERTIES_PATTERN; //NOI18N
Hashtable<String, Collection<? extends IndexResult>> fqnToResults = new Hashtable<String, Collection<? extends IndexResult>>();
fqnToResults.put(fqn, query(Index.FIELD_FQ_NAME, fqn + ".", QuerySupport.Kind.PREFIX, TERMS_BASIC_INFO)); //NOI18N
if (fqn.indexOf('.') == -1) {
Collection<? extends IndexResult> tmpResults = query(Index.FIELD_BASE_NAME, fqn, QuerySupport.Kind.EXACT, Index.FIELD_FQ_NAME);
for (IndexResult indexResult : tmpResults) {
String value = IndexedElement.getFQN(indexResult);
fqnToResults.put(value, query(Index.FIELD_FQ_NAME, value + ".", QuerySupport.Kind.PREFIX, TERMS_BASIC_INFO)); //NOI18N
}
}
for (Map.Entry<String, Collection<? extends IndexResult>> entry : fqnToResults.entrySet()) {
String fqnKey = entry.getKey();
Collection<? extends IndexResult> properties = entry.getValue();
for (IndexResult indexResult : properties) {
String value = indexResult.getValue(Index.FIELD_FQ_NAME);
if (!value.isEmpty() && value.charAt(value.length() - 1) != IndexedElement.PARAMETER_POSTFIX) {
value = value.substring(fqnKey.length());
if (value.lastIndexOf('.') == 0) {
IndexedElement property = IndexedElement.create(indexResult);
if (!property.getModifiers().contains(Modifier.PRIVATE)) {
result.add(property);
}
}
}
}
}
}
return result;
}
public Collection<? extends IndexResult> findByFqn(String fqn, String... fields) {
Collection<IndexResult> results = new ArrayList<IndexResult>();
results.addAll(query(Index.FIELD_FQ_NAME, fqn + IndexedElement.ANONYMOUS_POSFIX, QuerySupport.Kind.EXACT, fields)); //NOI18N
results.addAll(query(Index.FIELD_FQ_NAME, fqn + IndexedElement.OBJECT_POSFIX, QuerySupport.Kind.EXACT, fields)); //NOI18N
results.addAll(query(Index.FIELD_FQ_NAME, fqn + IndexedElement.PARAMETER_POSTFIX, QuerySupport.Kind.EXACT, fields)); //NOI18N
return results;
}
private String escapeRegExp(String text) {
return Pattern.quote(text);
}
private static class CacheKey {
private final Index index;
private final String fieldName;
private final String fieldValue;
private final QuerySupport.Kind kind;
public CacheKey(Index index, String fieldName, String fieldValue, QuerySupport.Kind kind) {
this.index = index;
this.fieldName = fieldName;
this.fieldValue = fieldValue;
this.kind = kind;
}
@Override
public int hashCode() {
int hash = 3;
hash = 41 * hash + (this.index != null ? this.index.hashCode() : 0);
hash = 41 * hash + (this.fieldName != null ? this.fieldName.hashCode() : 0);
hash = 41 * hash + (this.fieldValue != null ? this.fieldValue.hashCode() : 0);
hash = 41 * hash + (this.kind != null ? this.kind.hashCode() : 0);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final CacheKey other = (CacheKey) obj;
if (this.index != other.index && (this.index == null || !this.index.equals(other.index))) {
return false;
}
if ((this.fieldName == null) ? (other.fieldName != null) : !this.fieldName.equals(other.fieldName)) {
return false;
}
if ((this.fieldValue == null) ? (other.fieldValue != null) : !this.fieldValue.equals(other.fieldValue)) {
return false;
}
if (this.kind != other.kind) {
return false;
}
return true;
}
@Override
public String toString() {
return "CacheKey{" + "index=" + index + ", fieldName=" + fieldName + ", fieldValue=" + fieldValue + ", kind=" + kind + '}';
}
}
private static class CacheValue {
private final Set<String> fields;
private final Collection<? extends IndexResult> result;
public CacheValue(String[] fields, Collection<? extends IndexResult> result) {
this.fields = new HashSet<String>(Arrays.asList(fields));
this.result = result;
}
public Collection<? extends IndexResult> getResult() {
return result;
}
public boolean contains(String... fieldsToLoad) {
return fields.containsAll(Arrays.asList(fieldsToLoad));
}
}
private static class StatsKey {
private final String[] fields;
public StatsKey(String[] fields) {
this.fields = fields.clone();
Arrays.sort(this.fields);
}
@Override
public int hashCode() {
int hash = 3;
hash = 97 * hash + Arrays.deepHashCode(this.fields);
return hash;
}
@Override
public boolean equals(Object obj) {
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final StatsKey other = (StatsKey) obj;
if (!Arrays.deepEquals(this.fields, other.fields)) {
return false;
}
return true;
}
@Override
public String toString() {
return Arrays.deepToString(fields);
}
}
private static class StatsValue {
private final int requests;
private final int count;
private final long size;
public StatsValue(int requests, int count, long size) {
this.requests = requests;
this.count = count;
this.size = size;
}
public int getRequests() {
return requests;
}
public int getCount() {
return count;
}
public long getSize() {
return size;
}
@Override
public String toString() {
return "StatsValue{" + "requests=" + requests + ", average=" + (count != 0 ? (size / count) : 0)
+ ", count=" + count + ", size=" + size + '}';
}
}
}