/******************************************************************************* * Copyright (c) 2004, 2011 IBM Corporation and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * IBM Corporation - initial API and implementation *******************************************************************************/ package org.eclipse.wst.jsdt.web.core.javascript.search; import java.io.File; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IResourceChangeEvent; import org.eclipse.core.resources.IResourceChangeListener; import org.eclipse.core.resources.IResourceDelta; import org.eclipse.core.resources.IResourceDeltaVisitor; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Plugin; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.osgi.util.NLS; import org.eclipse.wst.jsdt.core.IJavaScriptProject; import org.eclipse.wst.jsdt.core.JavaScriptCore; import org.eclipse.wst.jsdt.internal.core.JavaModelManager; import org.eclipse.wst.jsdt.internal.core.search.indexing.IndexManager; import org.eclipse.wst.jsdt.web.core.internal.JsCoreMessages; import org.eclipse.wst.jsdt.web.core.internal.JsCorePlugin; import org.eclipse.wst.jsdt.web.core.internal.Logger; import org.eclipse.wst.jsdt.web.core.internal.project.JsWebNature; import org.eclipse.wst.jsdt.web.core.internal.validation.Util; import org.osgi.framework.Bundle; /** * * Provisional API: This class/interface is part of an interim API that is still under development and expected to * change significantly before reaching stability. It is being made available at this early stage to solicit feedback * from pioneering adopters on the understanding that any code that uses this API will almost certainly be broken * (repeatedly) as the API evolves. *(copied from JSP) * Responsible for keeping the JS index up to date. * */ public class JsIndexManager { // for debugging // TODO move this to Logger, as we have in SSE static final boolean DEBUG; static { String value = Platform.getDebugOption("org.eclipse.wst.jsdt.web.core/debug/jsindexmanager"); //$NON-NLS-1$ DEBUG = value != null && value.equalsIgnoreCase("true"); //$NON-NLS-1$ } private static final String PKEY_INDEX_STATE = "jspIndexState"; //$NON-NLS-1$ private IndexWorkspaceJob indexingJob = new IndexWorkspaceJob(); // TODO: consider enumeration for these int constants // set to S_UPDATING once a resource change comes in // set to S_STABLE if: // - we know we aren't interested in the resource change // - or the ProcessFilesJob completes // set to S_CANCELED if an indexing job is canceled // set to S_REBUILDING if re-indexing the entire workspace // the int '0' is reserved for the default value if a preference is not // there /** index is reliable to use */ public static final int S_STABLE = 1; /** index is being updated (from a resource delta) */ public static final int S_UPDATING = 2; /** entire index is being rebuilt */ public static final int S_REBUILDING = 3; /** * indexing job was canceled in the middle of it, index needs to be * rebuilt */ public static final int S_CANCELED = 4; /** symbolic name for OSGI framework */ private final String OSGI_FRAMEWORK_ID = "org.eclipse.osgi"; //$NON-NLS-1$ /** * Collects web page files from a resource delta. Derived resources should * be included to handle resources whose derived flag is being toggled. */ private class JSResourceDeltaVisitor implements IResourceDeltaVisitor { // using hash map ensures only one of each file // must be reset before every use private HashMap webPageFiles = null; public JSResourceDeltaVisitor() { this.webPageFiles = new HashMap(); } public boolean visit(IResourceDelta delta) throws CoreException { // in case JS search was canceled (eg. when closing the editor) if (JsSearchSupport.getInstance().isCanceled() || frameworkIsShuttingDown()) { setCanceledState(); return false; } try { // skip anything hidden by name if (isHiddenResource(delta.getFullPath())) { return false; } int kind = delta.getKind(); int flags = delta.getFlags(); boolean added = (kind & IResourceDelta.ADDED) == IResourceDelta.ADDED; boolean isInterestingChange = false; if ((kind & IResourceDelta.CHANGED) == IResourceDelta.CHANGED) { // ignore things like marker changes isInterestingChange = (flags & IResourceDelta.CONTENT) > 0 || (flags & IResourceDelta.REPLACED) > 0 || (flags & IResourceDelta.TYPE) > 0 || (flags & IResourceDelta.MOVED_FROM) > 0; } boolean removed = (kind & IResourceDelta.REMOVED) == IResourceDelta.REMOVED; if (added || isInterestingChange) { visitAdded(delta); } else if (removed) { visitRemoved(delta); } } catch (Exception e) { // need to set state here somehow, and reindex // otherwise index will be unreliable if (DEBUG) { Logger.logException("Delta analysis may not be complete", e); //$NON-NLS-1$ } } // if the delta has children, continue to add/remove files return true; } private void visitRemoved(IResourceDelta delta) { // handle cleanup if (delta.getResource() != null) { IResource r = delta.getResource(); if ((r.getType() == IResource.FOLDER) && r.exists()) { deleteIndex((IFile) r); } } } private void visitAdded(IResourceDelta delta) { // https://w3.opensource.ibm.com/bugzilla/show_bug.cgi?id=3553 // quick check if it's even web page related to improve // performance // checking name from the delta before getting // resource because it's lighter String filename = delta.getFullPath().lastSegment(); if (filename != null && Util.isJsType(filename)) { IResource r = delta.getResource(); if (r.isAccessible() && r.getType() == IResource.FILE) { this.webPageFiles.put(r.getFullPath(), r); } } } // https://bugs.eclipse.org/bugs/show_bug.cgi?id=93463 private boolean isHiddenResource(IPath p) { return p.segmentCount() > 0 && p.lastSegment().startsWith("."); } private void deleteIndex(IFile folder) { // cleanup index IndexManager im = JavaModelManager.getJavaModelManager().getIndexManager(); IPath folderPath = folder.getFullPath(); IPath indexLocation = JsSearchSupport.getInstance().computeIndexLocation(folderPath); im.removeIndex(indexLocation); // im.indexLocations.removeKey(folderPath); // im.indexLocations.removeValue(indexLocation); File f = indexLocation.toFile(); f.delete(); } public IFile[] getFiles() { return (IFile[]) this.webPageFiles.values().toArray(new IFile[this.webPageFiles.size()]); } public void reset() { this.webPageFiles.clear(); } } /** * schedules web pages for indexing */ private class ProcessFilesJob extends Job { List fileList = null; // keep track of how many files we've indexed int lastFileCursor = 0; ProcessFilesJob(String taskName) { super(taskName); fileList = new ArrayList(); } synchronized void process(IFile[] files) { for (int i = 0; i < files.length; i++) { fileList.add(files[i]); } if (DEBUG) { System.out.println("JSIndexManager queuing " + files.length + " files"); //$NON-NLS-2$ //$NON-NLS-1$ } } synchronized IFile[] getFiles() { return (IFile[]) fileList.toArray(new IFile[fileList.size()]); } synchronized void clearFiles() { fileList.clear(); lastFileCursor = 0; //System.out.println("cleared files"); } protected IStatus run(IProgressMonitor monitor) { // System.out.println("indexer monitor" + monitor); if (isCanceled(monitor) || frameworkIsShuttingDown()) { setCanceledState(); return Status.CANCEL_STATUS; } long start = System.currentTimeMillis(); try { IFile[] filesToBeProcessed = getFiles(); if (DEBUG) { System.out.println("JSIndexManager indexing " + filesToBeProcessed.length + " files"); //$NON-NLS-2$ //$NON-NLS-1$ } // API indicates that monitor is never null monitor.beginTask("", filesToBeProcessed.length); //$NON-NLS-1$ JsSearchSupport ss = JsSearchSupport.getInstance(); String processingNFiles = ""; //$NON-NLS-1$ for (;lastFileCursor < filesToBeProcessed.length; lastFileCursor++) { if (isCanceled(monitor) || frameworkIsShuttingDown()) { setCanceledState(); return Status.CANCEL_STATUS; } IFile file = filesToBeProcessed[lastFileCursor]; if(!file.isDerived()) { try { IJavaScriptProject project = JavaScriptCore.create(file.getProject()); if (project.exists()) { ss.addJspFile(file); // JS Indexer processing n files processingNFiles = NLS.bind(JsCoreMessages.JSPIndexManager_2, new String[]{Integer.toString((filesToBeProcessed.length - lastFileCursor))}); monitor.subTask(processingNFiles + " - " + file.getName()); //$NON-NLS-1$ monitor.worked(1); if (DEBUG) { System.out.println("JSIndexManager Job added file: " + file.getName()); //$NON-NLS-1$ } } } catch (Exception e) { // ISSUE: we probably shouldn't be catching EVERY // exception, but // the framework only allows to return IStatus in // order to communicate // that something went wrong, which means the loop // won't complete, and we would hit the same problem // the next time. // // a possible solution is to keep track of the // exceptions logged // and only log a certain amt of the same one, // otherwise skip it. if (!frameworkIsShuttingDown()) { String filename = file != null ? file.getFullPath().toString() : ""; //$NON-NLS-1$ Logger.logException("JSIndexer problem indexing:" + filename, e); //$NON-NLS-1$ } } } } // end for } finally { // just in case something didn't follow API (monitor is null) if (monitor != null) { monitor.done(); } } // successfully finished, clear files list clearFiles(); long finish = System.currentTimeMillis(); long diff = finish - start; if (DEBUG) { fTotalTime += diff; System.out.println("============================================================================"); //$NON-NLS-1$ System.out.println("this time: " + diff + " cumulative time for resource changed: " + fTotalTime); //$NON-NLS-1$ //$NON-NLS-2$ System.out.println("============================================================================"); //$NON-NLS-1$ } return Status.OK_STATUS; } private boolean isCanceled(IProgressMonitor runMonitor) { boolean canceled = false; // check specific monitor passed into run method (the progress // group in this case) // check main search support canceled if ((runMonitor != null) && runMonitor.isCanceled()) { canceled = true; } else if (JsSearchSupport.getInstance().isCanceled()) { canceled = true; if (runMonitor != null) { runMonitor.setCanceled(true); } } return canceled; } } // end class ProcessFilesJob private static JsIndexManager fSingleton = null; private boolean initialized; private boolean initializing = true; private IndexJobCoordinator indexJobCoordinator; private IResourceChangeListener jsResourceChangeListener; private JSResourceDeltaVisitor fVisitor = null; static long fTotalTime = 0; // Job for processing resource delta private ProcessFilesJob processFilesJob = null; private JsIndexManager() { processFilesJob = new ProcessFilesJob(JsCoreMessages.JSPIndexManager_0); // only show in verbose mode processFilesJob.setSystem(true); processFilesJob.setPriority(Job.LONG); processFilesJob.addJobChangeListener(new JobChangeAdapter() { public void done(IJobChangeEvent event) { super.done(event); setStableState(); } }); } public synchronized static JsIndexManager getInstance() { if (fSingleton == null) { fSingleton = new JsIndexManager(); } return fSingleton; } public void initialize() { JsIndexManager singleInstance = getInstance(); if (!singleInstance.initialized) { singleInstance.initialized = true; singleInstance.indexJobCoordinator = new IndexJobCoordinator(); singleInstance.jsResourceChangeListener = new JSResourceChangeListener(); // added as JobChange listener so JSIndexManager can be smarter // about when it runs Job.getJobManager().addJobChangeListener(singleInstance.indexJobCoordinator); // add JSIndexManager to keep JSP Index up to date // listening for IResourceChangeEvent.PRE_DELETE and // IResourceChangeEvent.POST_CHANGE ResourcesPlugin.getWorkspace().addResourceChangeListener(jsResourceChangeListener, IResourceChangeEvent.POST_CHANGE); // https://w3.opensource.ibm.com/bugzilla/show_bug.cgi?id=5091 // makes sure IndexManager is aware of our indexes saveIndexes(); //rebuildIndexIfNeeded(); singleInstance.initializing = false; } } synchronized void setIndexState(int state) { if (DEBUG) { System.out.println("JSIndexManager setting index state to: " + state2String(state)); //$NON-NLS-1$ } Plugin jspModelPlugin = JsCorePlugin.getDefault(); jspModelPlugin.getPluginPreferences().setValue(PKEY_INDEX_STATE, state); jspModelPlugin.savePluginPreferences(); } private String state2String(int state) { String s = "UNKNOWN"; //$NON-NLS-1$ switch (state) { case (S_STABLE) : s = "S_STABLE"; //$NON-NLS-1$ break; case (S_UPDATING) : s = "S_UPDATING"; //$NON-NLS-1$ break; case (S_CANCELED) : s = "S_CANCELED"; //$NON-NLS-1$ break; case (S_REBUILDING) : s = "S_REBUILDING"; //$NON-NLS-1$ break; } return s; } int getIndexState() { return JsCorePlugin.getDefault().getPluginPreferences().getInt(PKEY_INDEX_STATE); } void setUpdatingState() { //if (getIndexState() != S_CANCELED) setIndexState(S_UPDATING); } void setCanceledState() { setIndexState(JsIndexManager.S_CANCELED); } void setStableState() { //if (getIndexState() != S_CANCELED) setIndexState(S_STABLE); } void setRebuildingState() { setIndexState(S_REBUILDING); } synchronized void rebuildIndexIfNeeded() { if (getIndexState() != S_STABLE) { rebuildIndex(); } } void rebuildIndex() { if (DEBUG) { System.out.println("*** JS web page Index unstable, requesting re-indexing"); //$NON-NLS-1$ } getIndexingJob().addJobChangeListener(new JobChangeAdapter() { public void aboutToRun(IJobChangeEvent event) { super.aboutToRun(event); setRebuildingState(); } public void done(IJobChangeEvent event) { super.done(event); setStableState(); getIndexingJob().removeJobChangeListener(this); } }); // we're about to reindex everything anyway getProcessFilesJob().clearFiles(); getIndexingJob().schedule(); } /** * Creates and schedules a Job to process collected files. All JSP * indexing should be done through this method or processFiles(IFile file) * * @param files */ final void indexFiles(IFile[] files) { // don't use this rule // https://w3.opensource.ibm.com/bugzilla/show_bug.cgi?id=4931 // processFiles.setRule(new IndexFileRule()); processFilesJob.process(files); } /** * Package protected for access by inner Job class in resourceChanged(...) * * @return */ JSResourceDeltaVisitor getVisitor() { if (this.fVisitor == null) { this.fVisitor = new JSResourceDeltaVisitor(); } return this.fVisitor; } void saveIndexes() { IndexManager indexManager = JavaModelManager.getJavaModelManager().getIndexManager(); IProject[] allProjects = ResourcesPlugin.getWorkspace().getRoot().getProjects(); for (int j = 0; j < allProjects.length; j++) { if (!JsWebNature.hasNature(allProjects[j]) || !allProjects[j].isOpen()) { continue; } IPath jsModelWorkingLocation = JsSearchSupport.getInstance().getModelJspPluginWorkingLocation(allProjects[j]); File folder = new File(jsModelWorkingLocation.toOSString()); String[] files = folder.list(); String locay = ""; //$NON-NLS-1$ try { for (int i = 0; i < files.length; i++) { if (files[i].toLowerCase().endsWith(".index")) { //$NON-NLS-1$ locay = jsModelWorkingLocation.toString() + "/" + files[i]; //$NON-NLS-1$ // reuse index file // index = new Index(locay, allProjects[j].getFullPath().toOSString(), true); // //$NON-NLS-1$ // index.save(); indexManager.getIndex(allProjects[j].getFullPath(), new Path(locay), true, false); // indexManager.saveIndex(index); } } } catch (Exception e) { // we should be shutting down, want to shut down quietly if (JsIndexManager.DEBUG) { e.printStackTrace(); } } } } /** * A check to see if the OSGI framework is shutting down. * * @return true if the System Bundle is stopped (ie. the framework is * shutting down) */ boolean frameworkIsShuttingDown() { // in the Framework class there's a note: // set the state of the System Bundle to STOPPING. // this must be done first according to section 4.19.2 from the OSGi // R3 spec. boolean shuttingDown = Platform.getBundle(OSGI_FRAMEWORK_ID).getState() == Bundle.STOPPING; if (DEBUG && shuttingDown) { System.out.println("JSIndexManager: system is shutting down!"); //$NON-NLS-1$ } return shuttingDown; } public void shutdown() { // stop listening ResourcesPlugin.getWorkspace().removeResourceChangeListener(jsResourceChangeListener); // stop any searching JsSearchSupport.getInstance().setCanceled(true); // stop listening to jobs Job.getJobManager().removeJobChangeListener(indexJobCoordinator); int maxwait = 5000; if (processFilesJob != null) { processFilesJob.cancel(); } // attempt to make sure this indexing job is litterally // done before continuing, since we are shutting down waitTillNotRunning(maxwait, processFilesJob); if (indexingJob != null) { indexingJob.cancel(); } waitTillNotRunning(maxwait, processFilesJob); } private void waitTillNotRunning(int maxSeconds, Job job) { int pauseTime = 10; int maxtries = maxSeconds / pauseTime; int count = 0; while ((count++ < maxtries) && (job.getState() == Job.RUNNING)) { try { Thread.sleep(pauseTime); // System.out.println("count: " + count + " max: " + // maxtries); } catch (InterruptedException e) { Logger.logException(e); } } } private class IndexJobCoordinator extends JobChangeAdapter { public void aboutToRun(IJobChangeEvent event) { Job jobToCoordinate = event.getJob(); if (isJobToAvoid(jobToCoordinate)) { // job will be rescheduled when the job we // are avoiding (eg. build) is done getProcessFilesJob().cancel(); //System.out.println("cancel:" + jobToCoordinate.getName()); } } public void done(IJobChangeEvent event) { Job jobToCoordinate = event.getJob(); if (isJobToAvoid(jobToCoordinate)) { if (getProcessFilesJob().getFiles().length > 0) { getProcessFilesJob().schedule(500); //System.out.println("schedule:" + jobToCoordinate.getName()); } } } private boolean isJobToAvoid(Job jobToCoordinate) { boolean result = false; if (jobToCoordinate.belongsTo(ResourcesPlugin.FAMILY_AUTO_BUILD) || jobToCoordinate.belongsTo(ResourcesPlugin.FAMILY_MANUAL_BUILD) || jobToCoordinate.belongsTo(ResourcesPlugin.FAMILY_AUTO_REFRESH)) { result = true; } return result; } } private class JSResourceChangeListener implements IResourceChangeListener { /** * @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent) */ public void resourceChanged(IResourceChangeEvent event) { if (isInitializing()) { return; } // ignore resource changes if already rebuilding if (getIndexState() == S_REBUILDING) { return; } // previously canceled, needs entire index rebuild if (getIndexState() == S_CANCELED) { // rebuildIndex(); // just resume indexing getProcessFilesJob().schedule(500); //System.out.println("schedule: resource changed, previously canceled"); return; } IResourceDelta delta = event.getDelta(); if (delta != null) { // only care about adds or changes right now... int kind = delta.getKind(); boolean added = (kind & IResourceDelta.ADDED) == IResourceDelta.ADDED; boolean changed = (kind & IResourceDelta.CHANGED) == IResourceDelta.CHANGED; if (added || changed) { // only analyze the full (starting at root) delta // hierarchy if (delta.getFullPath().toString().equals("/")) { //$NON-NLS-1$ try { JSResourceDeltaVisitor v = getVisitor(); // clear from last run v.reset(); // count files, possibly do this in a job too... // don't include PHANTOM resources delta.accept(v, false); // process files from this delta IFile[] files = v.getFiles(); if (files.length > 0) { /* * Job change listener should set back to * stable when finished */ setUpdatingState(); // processFiles(files); indexFiles(files); } } catch (CoreException e) { // need to set state here somehow, and reindex // otherwise index will be unreliable if (DEBUG) { Logger.logException(e); } } catch (Exception e) { // need to set state here somehow, and reindex // otherwise index will be unreliable if (DEBUG) { Logger.logException(e); } } } } } } } IndexWorkspaceJob getIndexingJob() { return indexingJob; } ProcessFilesJob getProcessFilesJob() { return processFilesJob; } boolean isInitializing() { return initializing; } }