| /* |
| * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. |
| * |
| * Copyright 2009-2010 Sun Microsystems, Inc. All rights reserved. |
| * |
| * 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. Sun designates this |
| * particular file as subject to the "Classpath" exception as provided |
| * by Sun 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-2010 Sun Microsystems, Inc. |
| */ |
| |
| package org.netbeans.modules.jackpot30.impl.duplicates.indexing; |
| |
| import java.io.BufferedWriter; |
| import java.io.File; |
| import java.io.IOException; |
| import java.io.OutputStreamWriter; |
| import java.io.Writer; |
| import java.net.URL; |
| import java.util.Collection; |
| import java.util.Collections; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.Map.Entry; |
| import java.util.Set; |
| import java.util.concurrent.atomic.AtomicBoolean; |
| import java.util.logging.Level; |
| import java.util.logging.Logger; |
| import javax.swing.JButton; |
| import javax.swing.SwingUtilities; |
| import org.netbeans.api.annotations.common.NonNull; |
| import org.netbeans.api.java.classpath.ClassPath; |
| import org.netbeans.api.java.source.CancellableTask; |
| import org.netbeans.api.java.source.ClasspathInfo; |
| import org.netbeans.api.java.source.CompilationController; |
| import org.netbeans.api.java.source.CompilationInfo; |
| import org.netbeans.api.java.source.JavaSource; |
| import org.netbeans.api.java.source.JavaSource.Phase; |
| import org.netbeans.api.java.source.JavaSource.Priority; |
| import org.netbeans.api.java.source.JavaSourceTaskFactory; |
| import org.netbeans.api.java.source.Task; |
| import org.netbeans.api.progress.ProgressHandle; |
| import org.netbeans.api.progress.ProgressHandleFactory; |
| import org.netbeans.modules.parsing.impl.indexing.CacheFolder; |
| import org.netbeans.modules.parsing.impl.indexing.SPIAccessor; |
| import org.netbeans.modules.parsing.spi.indexing.Context; |
| import org.netbeans.modules.parsing.spi.indexing.CustomIndexer; |
| import org.netbeans.modules.parsing.spi.indexing.CustomIndexerFactory; |
| import org.netbeans.modules.parsing.spi.indexing.Indexable; |
| import org.openide.DialogDescriptor; |
| import org.openide.DialogDisplayer; |
| import org.openide.filesystems.FileAlreadyLockedException; |
| import org.openide.filesystems.FileObject; |
| import org.openide.filesystems.FileUtil; |
| import org.openide.filesystems.URLMapper; |
| import org.openide.util.Cancellable; |
| import org.openide.util.Exceptions; |
| import org.openide.util.Lookup; |
| import org.openide.util.lookup.ServiceProvider; |
| |
| /** |
| * |
| * @author lahvac |
| */ |
| public abstract class DeferredCustomIndexer extends CustomIndexer { |
| |
| private static final Logger LOG = Logger.getLogger(DeferredCustomIndexer.class.getName()); |
| |
| private final DeferredCustomIndexerFactory factory; |
| |
| protected DeferredCustomIndexer(DeferredCustomIndexerFactory factory) { |
| this.factory = factory; |
| } |
| |
| protected abstract void doIndex(DeferredContext ctx, Collection<? extends FileObject> modifiedAndAdded, Collection<? extends String> removed) throws IOException; |
| |
| @Override |
| protected final void index(Iterable<? extends Indexable> files, Context context) { |
| update(factory, context.getRootURI(), files, Collections.<Indexable>emptyList()); |
| } |
| |
| private static void dump(File where, Iterable<? extends String> lines) { |
| Writer out = null; |
| |
| try { |
| out = new BufferedWriter(new OutputStreamWriter(FileUtil.createData(where).getOutputStream(), "UTF-8")); |
| |
| for (String line : lines) { |
| out.write(line); |
| out.write("\n"); |
| } |
| } catch (FileAlreadyLockedException ex) { |
| Exceptions.printStackTrace(ex); |
| } catch (IOException ex) { |
| Exceptions.printStackTrace(ex); |
| } finally { |
| if (out != null) { |
| try { |
| out.close(); |
| } catch (IOException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| } |
| } |
| |
| private static Set<String> gatherRelativePaths(Iterable<? extends Indexable> it) { |
| Set<String> result = new HashSet<String>(); |
| |
| for (Indexable i : it) { |
| result.add(i.getRelativePath()); |
| } |
| |
| return result; |
| } |
| |
| private static void update(DeferredCustomIndexerFactory factory, URL root, Iterable<? extends Indexable> modified, Iterable<? extends Indexable> deleted) { |
| try { |
| Set<String> mod = gatherRelativePaths(modified); |
| Set<String> del = gatherRelativePaths(deleted); |
| |
| File cacheRoot = cacheRoot(root, factory); |
| |
| File modifiedFile = new File(cacheRoot, "modified"); |
| FileObject modifiedFileFO = FileUtil.toFileObject(modifiedFile); |
| Set<String> modifiedFiles = modifiedFileFO != null ? new HashSet<String>(modifiedFileFO.asLines("UTF-8")) : new HashSet<String>(); |
| boolean modifiedFilesChanged = modifiedFiles.removeAll(del); |
| |
| modifiedFilesChanged |= modifiedFiles.addAll(mod); |
| |
| if (modifiedFilesChanged) { |
| dump(modifiedFile, modifiedFiles); |
| } |
| |
| File deletedFile = new File(cacheRoot, "deleted"); |
| FileObject deletedFileFO = FileUtil.toFileObject(deletedFile); |
| Set<String> deletedFiles = deletedFileFO != null ? new HashSet<String>(deletedFileFO.asLines("UTF-8")) : new HashSet<String>(); |
| |
| boolean deletedFilesChanged = deletedFiles.removeAll(mod); |
| |
| deletedFilesChanged |= deletedFiles.addAll(del); |
| |
| if (deletedFilesChanged) { |
| dump(deletedFile, deletedFiles); |
| } |
| |
| if (!modifiedFiles.isEmpty() || !deletedFiles.isEmpty()) { |
| add2TODO(root, factory); |
| } |
| } catch (IOException ex) { |
| Exceptions.printStackTrace(ex); |
| } |
| } |
| |
| public static abstract class DeferredCustomIndexerFactory extends CustomIndexerFactory { |
| |
| public abstract DeferredCustomIndexer createIndexer(); |
| |
| @Override |
| public final void filesDeleted(Iterable<? extends Indexable> deleted, Context context) { |
| update(this, context.getRootURI(), Collections.<Indexable>emptyList(), deleted); |
| } |
| |
| @Override |
| public final void filesDirty(Iterable<? extends Indexable> dirty, Context context) {} |
| |
| @Override |
| public final boolean supportsEmbeddedIndexers() { |
| return false; |
| } |
| |
| @Override |
| public final void rootsRemoved(Iterable<? extends URL> removedRoots) { |
| super.rootsRemoved(removedRoots); |
| } |
| |
| @Override |
| public final void scanFinished(Context context) { |
| super.scanFinished(context); |
| } |
| |
| @Override |
| public final boolean scanStarted(Context context) { |
| return super.scanStarted(context); |
| } |
| |
| public void updateIndex(final URL root, final AtomicBoolean cancel) throws IOException { |
| final FileObject rootFO = URLMapper.findFileObject(root); |
| final ClasspathInfo cpInfo = ClasspathInfo.create(ClassPath.EMPTY, ClassPath.EMPTY, ClassPath.EMPTY); |
| |
| JavaSource.create(cpInfo).runUserActionTask(new Task<CompilationController>() { |
| public void run(CompilationController parameter) throws Exception { |
| if (cancel.get()) return ; |
| updateRoot(DeferredCustomIndexerFactory.this, root, rootFO, cancel); |
| } |
| }, true); |
| } |
| |
| } |
| |
| public static final class DeferredContext { |
| private final @NonNull URL root; |
| private final @NonNull FileObject rootFileObject; |
| private final @NonNull FileObject cacheRoot; |
| private final @NonNull Set<? extends FileObject> modifiedAndAdded; |
| private final @NonNull Set<? extends String> removed; |
| private final @NonNull AtomicBoolean cancel; |
| |
| public DeferredContext(URL root, FileObject rootFileObject, FileObject cacheRoot, Set<? extends FileObject> modifiedAndAdded, Set<? extends String> removed, AtomicBoolean cancel) { |
| this.root = root; |
| this.rootFileObject = rootFileObject; |
| this.cacheRoot = cacheRoot; |
| this.modifiedAndAdded = modifiedAndAdded; |
| this.removed = removed; |
| this.cancel = cancel; |
| } |
| |
| public @NonNull URL getRoot() { |
| return root; |
| } |
| |
| public @NonNull FileObject getRootFileObject() { |
| return rootFileObject; |
| } |
| |
| public FileObject getCacheRoot() { |
| return cacheRoot; |
| } |
| |
| public boolean isCancelled() { |
| return cancel.get(); |
| } |
| |
| public void handledModifiedFile(FileObject file) { |
| modifiedAndAdded.remove(file); |
| } |
| |
| public void handledRemovedFile(String relative) { |
| removed.remove(relative); |
| } |
| } |
| |
| /*return: true == done*/ |
| private static boolean updateRoot(DeferredCustomIndexerFactory factory, URL root, FileObject rootFO, AtomicBoolean cancel) throws IOException { |
| LOG.log(Level.FINE, "updating: {0}, for indexer: {1}", new Object[] {root.toExternalForm(), factory.getIndexerName()}); |
| File cacheRoot = cacheRoot(root, factory); |
| FileObject deletedFile = FileUtil.toFileObject(new File(cacheRoot, "deleted")); |
| Set<String> deletedFiles = deletedFile != null ? new HashSet<String>(deletedFile.asLines("UTF-8")) : Collections.<String>emptySet(); |
| |
| FileObject modifiedFile = FileUtil.toFileObject(new File(cacheRoot, "modified")); |
| Set<String> modifiedFiles = modifiedFile != null ? new HashSet<String>(modifiedFile.asLines("UTF-8")) : Collections.<String>emptySet(); |
| |
| Set<FileObject> toIndex = new HashSet<FileObject>(); |
| |
| for (String r : modifiedFiles) { |
| FileObject f = rootFO.getFileObject(r); |
| |
| if (f != null) { |
| toIndex.add(f); |
| } |
| } |
| |
| if (!toIndex.isEmpty() || !modifiedFiles.isEmpty()) { |
| factory.createIndexer().doIndex(new DeferredContext(root, rootFO, FileUtil.toFileObject(cacheRoot), toIndex, deletedFiles, cancel), new HashSet<FileObject>(toIndex), new HashSet<String>(deletedFiles)); |
| } |
| |
| boolean done = true; |
| |
| if (deletedFile != null) { |
| if (deletedFiles.isEmpty()) { |
| deletedFile.delete(); |
| } else { |
| dump(new File(cacheRoot, "deleted"), deletedFiles); |
| done = false; |
| } |
| } |
| if (modifiedFile != null) { |
| if (toIndex.isEmpty()) { |
| modifiedFile.delete(); |
| } else { |
| modifiedFiles.clear(); |
| |
| for (FileObject f : toIndex) { |
| modifiedFiles.add(FileUtil.getRelativePath(rootFO, f)); |
| } |
| |
| dump(new File(cacheRoot, "modified"), modifiedFiles); |
| done = false; |
| } |
| } |
| |
| return done; |
| } |
| |
| private static final Map<String, TODO> todo = new HashMap<String, TODO>(); //XXX: synchronization!!! |
| |
| private static void add2TODO(URL root, DeferredCustomIndexerFactory factory) { |
| if (DISABLED_INDEXERS.contains(factory.getIndexerName())) return; |
| |
| boolean wasEmpty = todo.isEmpty(); |
| TODO roots = todo.get(factory.getIndexerName()); |
| |
| if (roots == null) { |
| todo.put(factory.getIndexerName(), roots = new TODO(factory)); |
| } |
| |
| roots.roots.add(root); |
| |
| LOG.log(Level.FINE, "add2TODO, root: {0}, for factory: {1}, wasEmpty: {2}, todo: {3}", new Object[] {root.toExternalForm(), factory.getIndexerName(), wasEmpty, todo.toString()}); |
| |
| if (wasEmpty) RunAsNeededFactory.fileChanged(); |
| else RunAsNeededFactory.refresh(); |
| } |
| |
| private static File cacheRoot(URL root, CustomIndexerFactory factory) throws IOException { |
| FileObject indexBaseFolder = CacheFolder.getDataFolder(root); |
| String path = SPIAccessor.getInstance().getIndexerPath(factory.getIndexerName(), factory.getIndexVersion()); |
| FileObject indexFolder = FileUtil.createFolder(indexBaseFolder, path); |
| return FileUtil.toFile(indexFolder); |
| } |
| |
| private static final Set<String> DISABLED_INDEXERS = Collections.synchronizedSet(new HashSet<String>()); |
| |
| private static class UpdateWorker implements CancellableTask<CompilationInfo> { |
| |
| private static ProgressHandle progressForCurrentFactory; |
| private static DeferredCustomIndexerFactory currentFactory; |
| |
| private final AtomicBoolean cancel = new AtomicBoolean(); |
| |
| public void run(CompilationInfo parameter) throws Exception { |
| cancel.set(false); |
| |
| for (Iterator<Entry<String, TODO>> it = todo.entrySet().iterator(); it.hasNext();) { |
| if (cancel.get()) return; |
| |
| final Entry<String, TODO> e = it.next(); |
| |
| if (DISABLED_INDEXERS.contains(e.getKey())) { |
| it.remove(); |
| continue; |
| } |
| |
| if (currentFactory != e.getValue().factory) { |
| if (progressForCurrentFactory != null) { |
| progressForCurrentFactory.finish(); |
| } |
| |
| currentFactory = e.getValue().factory; |
| progressForCurrentFactory = ProgressHandleFactory.createSystemHandle("Background indexing for: " + currentFactory.getIndexerName(), new Cancellable() { |
| public boolean cancel() { |
| assert SwingUtilities.isEventDispatchThread(); |
| |
| JButton disableInThisSession = new JButton("Disable in This Session"); |
| JButton disablePermanently = new JButton("Disable Permanently"); |
| |
| disablePermanently.setEnabled(false); |
| |
| Object[] buttons = new Object[]{disableInThisSession, disablePermanently, DialogDescriptor.CANCEL_OPTION}; |
| DialogDescriptor dd = new DialogDescriptor("Disable background indexing for: " + e.getValue().factory.getIndexerName(), "Disable Background Indexing", true, buttons, disableInThisSession, DialogDescriptor.DEFAULT_ALIGN, null, null); |
| |
| dd.setClosingOptions(buttons); |
| |
| Object result = DialogDisplayer.getDefault().notify(dd); |
| |
| if (result == disableInThisSession) { |
| DISABLED_INDEXERS.add(e.getKey()); |
| return true; |
| } else if (result == disablePermanently) { |
| throw new UnsupportedOperationException(); |
| } else { |
| return false; |
| } |
| } |
| }); |
| |
| progressForCurrentFactory.start(); |
| } |
| |
| for (Iterator<URL> factIt = e.getValue().roots.iterator(); factIt.hasNext();) { |
| if (cancel.get()) return; |
| |
| URL root = factIt.next(); |
| FileObject rootFO = URLMapper.findFileObject(root); |
| |
| if (rootFO == null) { |
| //already deleted |
| it.remove(); |
| continue; |
| } |
| |
| if (updateRoot(e.getValue().factory, root, rootFO, cancel)) { |
| factIt.remove(); |
| } else { |
| if (!cancel.get()) { |
| LOG.log(Level.WARNING, "indexer: {0} did not update all files even if the process was not cancelled", currentFactory.getIndexerName()); |
| } |
| } |
| } |
| |
| if (e.getValue().roots.isEmpty()) |
| it.remove(); |
| |
| progressForCurrentFactory.finish(); |
| progressForCurrentFactory = null; |
| currentFactory = null; |
| } |
| |
| if (todo.isEmpty()) RunAsNeededFactory.fileChanged(); |
| } |
| |
| public void cancel() { |
| cancel.set(true); |
| } |
| } |
| |
| private static final class TODO { |
| final DeferredCustomIndexerFactory factory; |
| final Collection<URL> roots = new HashSet<URL>(); |
| TODO(DeferredCustomIndexerFactory factory) { |
| this.factory = factory; |
| } |
| } |
| |
| private static final boolean DEFERRED_INDEXER_ENABLED = Boolean.getBoolean(DeferredCustomIndexerFactory.class.getName() + ".enable"); |
| |
| private static final FileObject EMPTY_FILE; |
| |
| static { |
| try { |
| EMPTY_FILE = FileUtil.createMemoryFileSystem().getRoot().createData("empty.java"); |
| } catch (IOException ex) { |
| throw new IllegalStateException(ex); |
| } |
| } |
| |
| @ServiceProvider(service=JavaSourceTaskFactory.class) |
| public static final class RunAsNeededFactory extends JavaSourceTaskFactory { |
| |
| public RunAsNeededFactory() { |
| super(Phase.PARSED, Priority.MIN); |
| } |
| |
| @Override |
| protected CancellableTask<CompilationInfo> createTask(FileObject file) { |
| return new UpdateWorker(); |
| } |
| |
| @Override |
| protected Collection<FileObject> getFileObjects() { |
| return DEFERRED_INDEXER_ENABLED && !todo.isEmpty() ? Collections.singletonList(EMPTY_FILE) : Collections.<FileObject>emptyList(); |
| } |
| |
| public static void fileChanged() { |
| for (JavaSourceTaskFactory f : Lookup.getDefault().lookupAll(JavaSourceTaskFactory.class)) { |
| if (f instanceof RunAsNeededFactory) { |
| ((RunAsNeededFactory) f).fileObjectsChanged(); |
| } |
| } |
| } |
| |
| public static void refresh() { |
| for (JavaSourceTaskFactory f : Lookup.getDefault().lookupAll(JavaSourceTaskFactory.class)) { |
| if (f instanceof RunAsNeededFactory) { |
| ((RunAsNeededFactory) f).reschedule(EMPTY_FILE); |
| } |
| } |
| } |
| } |
| } |