blob: 4a08c5cbccf71e948487ab1773504d5b4ae91854 [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.parsing.impl;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.netbeans.api.annotations.common.NonNull;
import org.netbeans.api.editor.mimelookup.MimeLookup;
import org.netbeans.api.editor.mimelookup.MimePath;
import org.netbeans.modules.parsing.api.Embedding;
import org.netbeans.modules.parsing.api.Snapshot;
import org.netbeans.modules.parsing.api.Source;
import org.netbeans.modules.parsing.api.Task;
import org.netbeans.modules.parsing.spi.EmbeddingProvider;
import org.netbeans.modules.parsing.spi.ParseException;
import org.netbeans.modules.parsing.spi.Parser;
import org.netbeans.modules.parsing.spi.Parser.Result;
import org.netbeans.modules.parsing.spi.ParserFactory;
import org.netbeans.modules.parsing.spi.SchedulerEvent;
import org.netbeans.modules.parsing.spi.SchedulerTask;
import org.netbeans.modules.parsing.spi.SourceModificationEvent;
import org.netbeans.modules.parsing.spi.TaskFactory;
import org.netbeans.modules.parsing.spi.Scheduler;
import org.openide.filesystems.FileObject;
import org.openide.util.Lookup;
import org.openide.util.Pair;
import org.openide.util.WeakListeners;
/**
* This class maintains cache of parser instance, snapshot and nested embeddings
* for some block of code (or top level source).
*
* Threading: Instances of SourceCache and Source are synchronized using TaskProcessor.INTERNAL_LOCK
*
* @author Jan Jancura
*/
//@ThreadSafe
public final class SourceCache {
private static final Logger LOG = Logger.getLogger(SourceCache.class.getName());
private static final Comparator<SchedulerTask> PRIORITY_ORDER = new Comparator<SchedulerTask>() {
@Override
public int compare(final SchedulerTask o1, final SchedulerTask o2) {
final int p1 = o1.getPriority();
final int p2 = o2.getPriority();
return p1 < p2 ? -1 : p1 == p2 ? 0 : 1;
}
};
private final Source source;
//@GuardedBy(this)
private Embedding embedding;
private final String mimeType;
public SourceCache (
Source source,
Embedding embedding
) {
assert source != null;
this.source = source;
this.embedding = embedding;
mimeType = embedding != null ?
embedding.getMimeType () :
source.getMimeType ();
mimeType.getClass();
}
public SourceCache (
Source source,
Embedding embedding,
Parser parser
) {
this(source, embedding);
if (parser != null) {
this.parserInitialized = true;
this.parser = parser;
}
}
private void setEmbedding (
Embedding embedding
) {
synchronized (TaskProcessor.INTERNAL_LOCK) {
assert embedding.getMimeType ().equals (mimeType);
this.embedding = embedding;
snapshot = null;
}
}
//@GuardedBy(this)
private Snapshot snapshot;
public Snapshot getSnapshot () {
boolean isEmbedding;
synchronized (TaskProcessor.INTERNAL_LOCK) {
if (snapshot != null) {
return snapshot;
}
isEmbedding = embedding != null;
}
final Snapshot _snapshot = createSnapshot (isEmbedding);
synchronized (TaskProcessor.INTERNAL_LOCK) {
if (snapshot == null) {
snapshot = _snapshot;
}
return snapshot;
}
}
public @NonNull Source getSource() {
return this.source;
}
Snapshot createSnapshot (long[] idHolder) {
assert idHolder != null;
assert idHolder.length == 1;
boolean isEmbedding;
synchronized (TaskProcessor.INTERNAL_LOCK) {
isEmbedding = embedding != null;
}
idHolder[0] = SourceAccessor.getINSTANCE ().getLastEventId (this.source);
return createSnapshot (isEmbedding);
}
private Snapshot createSnapshot (boolean isEmbedding) {
Snapshot _snapshot = isEmbedding ? embedding.getSnapshot () : source.createSnapshot ();
assert mimeType.equals (_snapshot.getMimeType ());
return _snapshot;
}
//@GuarderBy(this)
private boolean parserInitialized = false;
//@GuardedBy(this)
private Parser parser;
public Parser getParser () {
synchronized (TaskProcessor.INTERNAL_LOCK) {
if (parserInitialized) {
return parser;
}
}
Parser _parser = null;
Lookup lookup = MimeLookup.getLookup (mimeType);
ParserFactory parserFactory = lookup.lookup (ParserFactory.class);
if (parserFactory != null) {
final Snapshot _snapshot = getSnapshot ();
final Collection<Snapshot> _tmp = Collections.singleton (_snapshot);
_parser = parserFactory.createParser (_tmp);
if (_parser == null) {
LOG.log(
Level.INFO,
"Parser factory: {0} returned null parser for {1}", //NOI18N
new Object[]{
parserFactory,
_snapshot
});
}
}
synchronized (TaskProcessor.INTERNAL_LOCK) {
if (!parserInitialized) {
parser = _parser;
if (parser != null) {
parser.addChangeListener(WeakListeners.change(
SourceAccessor.getINSTANCE().getParserEventForward(source),
parser));
}
parserInitialized = true;
}
return parser;
}
}
//@GuardedBy(this)
private boolean parsed = false;
public Result getResult (
final Task task
) throws ParseException {
assert TaskProcessor.holdsParserLock();
Parser _parser = getParser ();
if (_parser == null) return null;
boolean _parsed;
synchronized (TaskProcessor.INTERNAL_LOCK) {
_parsed = this.parsed;
this.parsed = true; //Optimizstic update
}
if (!_parsed) {
boolean parseSuccess = false;
try {
final Snapshot _snapshot = getSnapshot ();
final FileObject file = _snapshot.getSource().getFileObject();
if (file != null && !file.isValid()) {
return null;
}
SourceModificationEvent event = SourceAccessor.getINSTANCE ().getSourceModificationEvent (source);
TaskProcessor.callParse(_parser, snapshot, task, event);
SourceAccessor.getINSTANCE ().parsed (source);
parseSuccess = true;
} finally {
if (!parseSuccess) {
synchronized (TaskProcessor.INTERNAL_LOCK) {
parsed = false;
}
}
}
}
return TaskProcessor.callGetResult(_parser, task);
}
public void invalidate () {
synchronized (TaskProcessor.INTERNAL_LOCK) {
snapshot = null;
parsed = false;
embeddings = null;
providerOrder = null;
upToDateEmbeddingProviders.clear();
for (SourceCache sourceCache : embeddingToCache.values ())
sourceCache.invalidate ();
}
}
public void invalidate (final Snapshot preRendered) {
synchronized (TaskProcessor.INTERNAL_LOCK) {
invalidate();
snapshot = preRendered;
}
}
//@GuardedBy(this)
private volatile List<Embedding>
embeddings;
//@GuardedBy(this)
private List<EmbeddingProvider>
providerOrder;
//@GuardedBy(this)
private final Map<EmbeddingProvider,List<Embedding>>
embeddingProviderToEmbedings = new HashMap<EmbeddingProvider,List<Embedding>> ();
public Iterable<Embedding> getAllEmbeddings () {
List<Embedding> result = this.embeddings;
if (result != null) {
return result;
}
retry: while (true) {
final Snapshot snpsht = getSnapshot();
final Collection<SchedulerTask> tsks = createTasks();
final Set<EmbeddingProvider> currentUpToDateProviders;
synchronized (TaskProcessor.INTERNAL_LOCK) {
//BEGIN
currentUpToDateProviders = new HashSet<EmbeddingProvider>(upToDateEmbeddingProviders);
}
final Map<EmbeddingProvider,List<Embedding>> newEmbeddings = new LinkedHashMap<EmbeddingProvider, List<Embedding>> ();
for (SchedulerTask schedulerTask : tsks) {
if (schedulerTask instanceof EmbeddingProvider) {
final EmbeddingProvider embeddingProvider = (EmbeddingProvider) schedulerTask;
if (newEmbeddings.containsKey(embeddingProvider)) {
LOG.log(Level.WARNING, "EmbeddingProvider: {0} is registered several time.", embeddingProvider); //NOI18N
}
else if (currentUpToDateProviders.contains(embeddingProvider)) {
newEmbeddings.put(embeddingProvider,null);
} else {
final List<Embedding> embForProv = TaskProcessor.callEmbeddingProvider(embeddingProvider,snpsht);
if (embForProv == null) {
throw new NullPointerException(String.format("The %s returned null embeddings!", embeddingProvider));
}
newEmbeddings.put(embeddingProvider, embForProv);
}
}
}
synchronized (TaskProcessor.INTERNAL_LOCK) {
if (this.embeddings == null) {
if (!upToDateEmbeddingProviders.equals(currentUpToDateProviders)) {
//ROLLBACK and RETRY
continue retry;
}
result = new LinkedList<Embedding> ();
for (Map.Entry<EmbeddingProvider,List<Embedding>> entry : newEmbeddings.entrySet()) {
final EmbeddingProvider embeddingProvider = entry.getKey();
final List<Embedding> embeddingsForProvider = entry.getValue();
if (embeddingsForProvider == null) {
List<Embedding> _embeddings = embeddingProviderToEmbedings.get (embeddingProvider);
result.addAll (_embeddings);
} else {
List<Embedding> oldEmbeddings = embeddingProviderToEmbedings.get (embeddingProvider);
updateEmbeddings (embeddingsForProvider, oldEmbeddings, false, null);
embeddingProviderToEmbedings.put (embeddingProvider, embeddingsForProvider);
upToDateEmbeddingProviders.add (embeddingProvider);
result.addAll (embeddingsForProvider);
}
}
//COMMIT
this.embeddings = result;
this.providerOrder = new ArrayList<EmbeddingProvider>(newEmbeddings.keySet());
}
return this.embeddings;
}
}
}
//@GuardedBy(this)
private final Set<EmbeddingProvider> upToDateEmbeddingProviders = new HashSet<EmbeddingProvider> ();
void refresh (EmbeddingProvider embeddingProvider, Class<? extends Scheduler> schedulerType) {
List<Embedding> _embeddings = TaskProcessor.callEmbeddingProvider(embeddingProvider, getSnapshot ());
List<Embedding> oldEmbeddings;
synchronized (TaskProcessor.INTERNAL_LOCK) {
oldEmbeddings = embeddingProviderToEmbedings.get (embeddingProvider);
updateEmbeddings (_embeddings, oldEmbeddings, true, schedulerType);
embeddingProviderToEmbedings.put (embeddingProvider, _embeddings);
upToDateEmbeddingProviders.add (embeddingProvider);
if (this.embeddings != null) {
final Embedding insertBefore = findNextEmbedding(providerOrder, embeddingProviderToEmbedings, embeddingProvider);
this.embeddings.removeAll(oldEmbeddings);
int index = insertBefore == null ? -1 : this.embeddings.indexOf(insertBefore);
this.embeddings.addAll(index == -1 ? this.embeddings.size() : index, _embeddings);
}
}
}
private Embedding findNextEmbedding (
final List<EmbeddingProvider> order,
final Map<EmbeddingProvider,List<Embedding>> providers2embs,
final EmbeddingProvider provider) {
if (order != null) {
boolean accept = false;
for (EmbeddingProvider p : order) {
if (accept) {
final Collection<Embedding> c = providers2embs.get(p);
if (c != null && !c.isEmpty()) {
return c.iterator().next();
}
} else if (p.equals(provider)) {
accept = true;
}
}
}
return null;
}
private void updateEmbeddings (
final List<Embedding> embeddings,
final List<Embedding> oldEmbeddings,
final boolean updateTasks,
final Class<? extends Scheduler> schedulerType) {
assert Thread.holdsLock(TaskProcessor.INTERNAL_LOCK);
final List<SourceCache> toBeSchedulled = new ArrayList<SourceCache> ();
if (oldEmbeddings != null && embeddings.size () == oldEmbeddings.size ()) {
for (int i = 0; i < embeddings.size (); i++) {
if (embeddings.get (i) == null) {
throw new NullPointerException ();
}
SourceCache cache = embeddingToCache.remove (oldEmbeddings.get (i));
if (cache != null) {
cache.setEmbedding (embeddings.get (i));
assert embeddings.get (i).getMimeType ().equals (cache.getSnapshot ().getMimeType ());
embeddingToCache.put (embeddings.get (i), cache);
} else {
cache = getCache(embeddings.get(i));
}
if (updateTasks) {
toBeSchedulled.add (cache);
}
}
} else {
if (oldEmbeddings != null) {
for (Embedding _embedding : oldEmbeddings) {
SourceCache cache = embeddingToCache.remove (_embedding);
if (cache != null) {
cache.removeTasks ();
}
}
}
if (updateTasks) {
for (Embedding _embedding : embeddings) {
SourceCache cache = getCache (_embedding);
toBeSchedulled.add (cache);
}
}
}
for (SourceCache cache : toBeSchedulled) {
cache.scheduleTasks (schedulerType);
}
}
//@GuardedBy(this)
private final Map<Embedding,SourceCache>
embeddingToCache = new HashMap<Embedding,SourceCache> ();
public SourceCache getCache (Embedding embedding) {
synchronized (TaskProcessor.INTERNAL_LOCK) {
SourceCache sourceCache = embeddingToCache.get (embedding);
if (sourceCache == null) {
sourceCache = new SourceCache (source, embedding);
assert embedding.getMimeType ().equals (sourceCache.getSnapshot ().getMimeType ());
embeddingToCache.put (embedding, sourceCache);
}
return sourceCache;
}
}
// tasks management ........................................................
//@GuardedBy(this)
private List<SchedulerTask>
tasks;
//@GuardedBy(this)
private Set<SchedulerTask>
pendingTasks;
private Collection<SchedulerTask> createTasks () {
List<SchedulerTask> tasks1 = null;
Set<SchedulerTask> pendingTasks1 = null;
if (tasks == null) {
tasks1 = new ArrayList<SchedulerTask> ();
pendingTasks1 = new HashSet<SchedulerTask> ();
Lookup lookup = MimeLookup.getLookup (mimeType);
Collection<? extends TaskFactory> factories = lookup.lookupAll (TaskFactory.class);
Snapshot fakeSnapshot = null;
for (TaskFactory factory : factories) {
// #185586 - this is here in order not to create snapshots (a copy of file/doc text)
// if there is no task that would really need it (eg. in C/C++ projects there is no parser
// registered and no tasks will ever run on these files, even though there may be tasks
// registered for all mime types
Collection<? extends SchedulerTask> newTasks = factory.create(getParser() != null ?
getSnapshot() :
fakeSnapshot == null ?
fakeSnapshot = SourceAccessor.getINSTANCE().createSnapshot(
"", //NOI18N
new int [] { 0 },
source,
MimePath.get (mimeType),
new int[][] {new int[] {0, 0}},
new int[][] {new int[] {0, 0}}) :
fakeSnapshot
);
if (newTasks != null) {
tasks1.addAll (newTasks);
pendingTasks1.addAll (newTasks);
}
}
Collections.sort(tasks1, PRIORITY_ORDER);
}
synchronized (TaskProcessor.INTERNAL_LOCK) {
if ((tasks == null) && (tasks1 != null)) {
tasks = tasks1;
pendingTasks = pendingTasks1;
}
if (tasks != null) {
// this should be normal return in most cases
return tasks;
}
}
// recurse and hope
return createTasks();
}
//tzezula: probably has race condition
public void scheduleTasks (Class<? extends Scheduler> schedulerType) {
//S ystem.out.println("scheduleTasks " + schedulerType);
final List<SchedulerTask> remove = new ArrayList<SchedulerTask> ();
final List<Pair<SchedulerTask,Class<? extends Scheduler>>> add = new ArrayList<Pair<SchedulerTask,Class<? extends Scheduler>>> ();
Collection<SchedulerTask> tsks = createTasks();
synchronized (TaskProcessor.INTERNAL_LOCK) {
for (SchedulerTask task : tsks) {
if (schedulerType == null || task instanceof EmbeddingProvider) {
if (!pendingTasks.remove (task)) {
remove.add (task);
}
add.add (Pair.<SchedulerTask,Class<? extends Scheduler>>of(task,null));
} else if (task.getSchedulerClass () == schedulerType) {
if (!pendingTasks.remove (task)) {
remove.add (task);
}
add.add (Pair.<SchedulerTask,Class<? extends Scheduler>>of(task,schedulerType));
}
}
}
if (!add.isEmpty ()) {
LOG.fine("Change tasks for source " + source + " - add: " + add + ", remove " + remove);
TaskProcessor.updatePhaseCompletionTask(add, remove, source, this);
}
}
public void unscheduleTasks (Class<? extends Scheduler> schedulerType) {
//S ystem.out.println("unscheduleTasks " + schedulerType);
final List<SchedulerTask> remove = new ArrayList<SchedulerTask> ();
synchronized (TaskProcessor.INTERNAL_LOCK) {
if (tasks != null) {
for (SchedulerTask task : tasks) {
if (schedulerType == null ||
task.getSchedulerClass () == schedulerType ||
task instanceof EmbeddingProvider
) {
remove.add (task);
}
}
}
}
if (!remove.isEmpty ()) {
TaskProcessor.removePhaseCompletionTasks(remove, source);
}
}
//jjancura: probably has race condition too
public void sourceModified () {
SourceModificationEvent sourceModificationEvent = SourceAccessor.getINSTANCE ().getSourceModificationEvent (source);
if (sourceModificationEvent == null)
return;
final Map<Class<? extends Scheduler>,? extends SchedulerEvent> schedulerEvents =
SourceAccessor.getINSTANCE ().createSchedulerEvents (source,
Utilities.getEnvFactory().getSchedulers(source.getLookup()), sourceModificationEvent);
if (schedulerEvents.isEmpty ()) {
return;
}
final List<SchedulerTask> remove = new ArrayList<SchedulerTask> ();
final List<Pair<SchedulerTask,Class<? extends Scheduler>>> add = new ArrayList<Pair<SchedulerTask,Class<? extends Scheduler>>>();
Collection<SchedulerTask> tsks = createTasks();
synchronized (TaskProcessor.INTERNAL_LOCK) {
for (SchedulerTask task : tsks) {
Class<? extends Scheduler> schedulerClass = task.getSchedulerClass ();
if (schedulerClass != null &&
!schedulerEvents.containsKey (schedulerClass)
)
continue;
if (!pendingTasks.remove (task)) {
remove.add (task);
}
add.add (Pair.<SchedulerTask,Class<? extends Scheduler>>of(task,null));
}
}
if (!add.isEmpty ()) {
TaskProcessor.updatePhaseCompletionTask (add, remove, source, this);
}
}
@Override
public String toString () {
StringBuilder sb = new StringBuilder ("SourceCache ");
sb.append (hashCode ());
sb.append (": ");
Snapshot _snapshot = getSnapshot ();
Source _source = _snapshot.getSource ();
FileObject fileObject = _source.getFileObject ();
if (fileObject != null)
sb.append (fileObject.getNameExt ());
else
sb.append (mimeType).append (" ").append (_source.getDocument (false));
if (!_snapshot.getMimeType ().equals (_source.getMimeType ())) {
sb.append ("( ").append (_snapshot.getMimeType ()).append (" ");
sb.append (_snapshot.getOriginalOffset (0)).append ("-").append (_snapshot.getOriginalOffset (_snapshot.getText ().length () - 1)).append (")");
}
return sb.toString ();
}
//@NotThreadSafe - has to be called in GuardedBy(this)
private void removeTasks () {
if (tasks != null) {
TaskProcessor.removePhaseCompletionTasks (tasks, source);
}
tasks = null;
}
}