blob: ccdfccf60565922e9c264f423e4df7465c2a54ec [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.html.editor.api.index;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringTokenizer;
import java.util.WeakHashMap;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.event.ChangeListener;
import org.netbeans.api.project.Project;
import org.netbeans.modules.parsing.spi.indexing.support.IndexResult;
import org.netbeans.modules.parsing.spi.indexing.support.QuerySupport;
import org.netbeans.modules.web.common.api.DependenciesGraph;
import org.netbeans.modules.web.common.api.DependenciesGraph.Node;
import org.netbeans.modules.web.common.api.FileReference;
import org.netbeans.modules.web.common.api.WebUtils;
import org.openide.filesystems.FileObject;
import org.openide.util.ChangeSupport;
import org.openide.util.Exceptions;
/**
* An instance of the indexer which can be held until the source roots are valid.
*
* TODO: Release the cached value of html index once any of the underlying data
* models changes (mainly the classpath).
*
* @author marekfukala
*/
public class HtmlIndex {
/**
* Name of the index file.
*/
public static final String NAME = "html"; //NOI18N
/**
* Index version.
*/
public static final int VERSION = 2;
/**
* Name of the field with references.
*/
public static final String REFERS_KEY = "imports"; //NOI18N
private static final Map<Project, HtmlIndex> INDEXES = new WeakHashMap<>();
/**
* Returns per-project cached instance of HtmlIndex
*
*/
public static HtmlIndex get(Project project) throws IOException {
return get(project, true);
}
public static HtmlIndex get(Project project, boolean createIfNeccesary) throws IOException {
if(project == null) {
throw new NullPointerException();
}
synchronized (INDEXES) {
HtmlIndex index = INDEXES.get(project);
if(index == null && createIfNeccesary) {
index = new HtmlIndex(project);
INDEXES.put(project, index);
}
return index;
}
}
private final QuerySupport querySupport;
private ChangeSupport changeSupport;
/** Creates a new instance of JsfIndex */
private HtmlIndex(Project project) throws IOException {
//QuerySupport now refreshes the roots indexes so it can held until the source roots are valid
Collection<FileObject> sourceRoots = QuerySupport.findRoots(project,
null /* all source roots */,
Collections.<String>emptyList(),
Collections.<String>emptyList());
this.querySupport = QuerySupport.forRoots(NAME, VERSION, sourceRoots.toArray(new FileObject[]{}));
this.changeSupport = new ChangeSupport(this);
}
public void addChangeListener(ChangeListener l) {
changeSupport.addChangeListener(l);
}
public void removeChangeListener(ChangeListener l) {
changeSupport.addChangeListener(l);
}
// TODO: should not be in the API; for now it is OK; need to talk to Marek
// whether this approach to notification of changes makes any sense or should
// be done completely differently
public void notifyChange() {
changeSupport.fireChange();
}
/**
*
* @param keyName
* @param value
* @return returns a collection of files which contains the keyName key and the
* value matches the value regular expression
*/
public Collection<FileObject> find(String keyName, String value) {
try {
String searchExpression = ".*(" + value + ")[,;].*"; //NOI18N
Collection<FileObject> matchedFiles = new LinkedList<>();
Collection<? extends IndexResult> results = querySupport.query(keyName, searchExpression, QuerySupport.Kind.REGEXP, keyName);
for (IndexResult result : filterDeletedFiles(results)) {
matchedFiles.add(result.getFile());
}
return matchedFiles;
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return Collections.emptyList();
}
/**
* Gets two maps wrapped in the AllDependenciesMaps class which contains
* all dependencies defined by imports in the current project.
*
* @return instance of AllDependenciesMaps
* @throws IOException
*/
public AllDependenciesMaps getAllDependencies() throws IOException {
Collection<? extends IndexResult> results = filterDeletedFiles(querySupport.query(
REFERS_KEY, "", QuerySupport.Kind.PREFIX, REFERS_KEY));
Map<FileObject, Collection<FileReference>> source2dests = new HashMap<>();
Map<FileObject, Collection<FileReference>> dest2sources = new HashMap<>();
for (IndexResult result : results) {
String importsValue = result.getValue(REFERS_KEY);
if (importsValue != null) {
FileObject file = result.getFile();
Collection<String> imports = decodeListValue(importsValue);
Collection<FileReference> imported = new HashSet<>();
for (String importedFileName : imports) {
//resolve the file
FileReference resolvedReference = WebUtils.resolveToReference(file, importedFileName);
// FileObject resolvedFileObject = ref.target();
if (resolvedReference != null) {
imported.add(resolvedReference);
//add reverse dependency
Collection<FileReference> sources = dest2sources.get(resolvedReference.target());
if (sources == null) {
sources = new HashSet<>();
dest2sources.put(resolvedReference.target(), sources);
}
sources.add(resolvedReference);
}
}
source2dests.put(file, imported);
}
}
return new AllDependenciesMaps(source2dests, dest2sources);
}
/**
* Returns list of all remote URLs
*/
public List<URL> getAllRemoteDependencies() throws IOException {
Collection<? extends IndexResult> results = filterDeletedFiles(querySupport.query(REFERS_KEY, "", QuerySupport.Kind.PREFIX, REFERS_KEY));
Set<String> paths = new HashSet<>();
for (IndexResult result : results) {
String importsValue = result.getValue(REFERS_KEY);
if(importsValue != null) {
paths.addAll(decodeListValue(importsValue));
}
}
List<URL> urls = new ArrayList<>();
for (String p : paths) {
// #215468 - better handling of protocol-relative JavaScript files:
if (p.startsWith("//")) { // NOI18N
p = "http:" + p; // NOI18N
}
// TODO: any better way to pick only remote URLs?
if (p.startsWith("http")) { // NOI18N
try {
urls.add(new URL(p));
} catch (MalformedURLException ex) {
// #219118 - ignore invalid URLs silently
}
}
}
return urls;
}
/**
* Gets all 'related' files to the given html file object.
*
* @param htmlFile
* @return a collection of all files which either imports or are imported
* by the given htmlFile both directly and indirectly (transitive relation)
*/
public DependenciesGraph getDependencies(FileObject cssFile) {
try {
DependenciesGraph deps = new DependenciesGraph(cssFile);
AllDependenciesMaps alldeps = getAllDependencies();
resolveDependencies(deps.getSourceNode(), alldeps.getSource2dest(), alldeps.getDest2source());
return deps;
} catch (IOException ex) {
Exceptions.printStackTrace(ex);
}
return null;
}
private void resolveDependencies(Node base, Map<FileObject, Collection<FileReference>> source2dests, Map<FileObject, Collection<FileReference>> dest2sources) {
FileObject baseFile = base.getFile();
Collection<FileReference> destinations = source2dests.get(baseFile);
if (destinations != null) {
//process destinations (file this one refers to)
for(FileReference destinationReference : destinations) {
FileObject destination = destinationReference.target();
Node node = base.getDependencyGraph().getNode(destination);
if(base.addReferedNode(node)) {
//recurse only if we haven't been there yet
resolveDependencies(node, source2dests, dest2sources);
}
}
}
Collection<FileReference> sources = dest2sources.get(baseFile);
if(sources != null) {
//process sources (file this one is refered by)
for(FileReference sourceReference : sources) {
FileObject source = sourceReference.source();
Node node = base.getDependencyGraph().getNode(source);
if(base.addReferingNode(node)) {
//recurse only if we haven't been there yet
resolveDependencies(node, source2dests, dest2sources);
}
}
}
}
//each list value is terminated by semicolon
private Collection<String> decodeListValue(String value) {
assert value.charAt(value.length() - 1) == ';';
Collection<String> list = new ArrayList<>();
StringTokenizer st = new StringTokenizer(value.substring(0, value.length() - 1), ",");
while (st.hasMoreTokens()) {
list.add(st.nextToken());
}
return list;
}
//if an indexed file is delete and IndexerFactory.filesDeleted() hasn't removed
//the entris from index yet, then we may receive IndexResult-s with null file.
//Please note that the IndexResult.getFile() result is cached, so the IndexResult.getFile()
//won't become null after the query is run, but the file will simply become invalid.
private Collection<? extends IndexResult> filterDeletedFiles(Collection<? extends IndexResult> queryResult) {
Collection<IndexResult> filtered = new ArrayList<>();
for(IndexResult result : queryResult) {
if(result.getFile() != null) {
filtered.add(result);
}
}
return filtered;
}
public static class AllDependenciesMaps {
Map<FileObject, Collection<FileReference>> source2dest, dest2source;
public AllDependenciesMaps(Map<FileObject, Collection<FileReference>> source2dest, Map<FileObject, Collection<FileReference>> dest2source) {
this.source2dest = source2dest;
this.dest2source = dest2source;
}
/**
*
* @return reversed map of getSource2dest() (imported file -> collection of
* importing files)
*/
public Map<FileObject, Collection<FileReference>> getDest2source() {
return dest2source;
}
/**
*
* @return map of fileobject -> collection of fileobject(s) describing
* relations between css file defined by import directive. The key represents
* a fileobject which imports the files from the value's collection.
*/
public Map<FileObject, Collection<FileReference>> getSource2dest() {
return source2dest;
}
}
}