/******************************************************************************* * Copyright (c) 2000, 2006 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.rubypeople.rdt.internal.core; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IResource; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IPath; import org.eclipse.core.runtime.Path; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.rubypeople.rdt.core.ILoadpathEntry; import org.rubypeople.rdt.core.IRubyElement; import org.rubypeople.rdt.core.IRubyElementDelta; import org.rubypeople.rdt.core.IRubyModel; import org.rubypeople.rdt.core.IRubyModelStatus; import org.rubypeople.rdt.core.IRubyProject; import org.rubypeople.rdt.core.ISourceFolder; import org.rubypeople.rdt.core.ISourceFolderRoot; import org.rubypeople.rdt.core.RubyModelException; import org.rubypeople.rdt.internal.compiler.util.ObjectVector; import org.rubypeople.rdt.internal.core.search.indexing.IndexManager; import org.rubypeople.rdt.internal.core.util.Messages; import org.rubypeople.rdt.internal.core.util.Util; /** * This operation sets an <code>IRubyProject</code>'s classpath. * * @see IRubyProject */ public class SetLoadpathOperation extends RubyModelOperation { ILoadpathEntry[] oldResolvedPath, newResolvedPath; ILoadpathEntry[] newRawPath; boolean canChangeResources; boolean loadpathWasSaved; boolean needCycleCheck; boolean needValidation; boolean needSave; IPath newOutputLocation; RubyProject project; boolean identicalRoots; /* * Used to indicate that the classpath entries remain the same. */ public static final ILoadpathEntry[] DO_NOT_SET_ENTRIES = new ILoadpathEntry[0]; public static final ILoadpathEntry[] DO_NOT_UPDATE_PROJECT_REFS = new ILoadpathEntry[0]; /* * Used to indicate that the output location remains the same. */ public static final IPath DO_NOT_SET_OUTPUT = new Path("Reuse Existing Output Location"); //$NON-NLS-1$ /** * When executed, this operation sets the classpath of the given project. */ public SetLoadpathOperation( RubyProject project, ILoadpathEntry[] oldResolvedPath, ILoadpathEntry[] newRawPath, IPath newOutputLocation, boolean canChangeResource, boolean needValidation, boolean needSave) { super(new IRubyElement[] { project }); this.oldResolvedPath = oldResolvedPath; this.newRawPath = newRawPath; this.newOutputLocation = newOutputLocation; this.canChangeResources = canChangeResource; this.needValidation = needValidation; this.needSave = needSave; this.project = project; } /** * Adds deltas for the given roots, with the specified change flag, * and closes the root. Helper method for #setLoadpath */ protected void addLoadpathDeltas( ISourceFolderRoot[] roots, int flag, RubyElementDelta delta) { for (int i = 0; i < roots.length; i++) { ISourceFolderRoot root = roots[i]; delta.changed(root, flag); if ((flag & IRubyElementDelta.F_REMOVED_FROM_CLASSPATH) != 0 || (flag & IRubyElementDelta.F_SOURCEATTACHED) != 0 || (flag & IRubyElementDelta.F_SOURCEDETACHED) != 0){ try { root.close(); } catch (RubyModelException e) { // ignore } } } } protected boolean canModifyRoots() { // setting classpath can modify roots return true; } /** * Returns the index of the item in the list if the given list contains the specified entry. If the list does * not contain the entry, -1 is returned. * A helper method for #setLoadpath */ protected int classpathContains( ILoadpathEntry[] list, ILoadpathEntry entry) { IPath[] exclusionPatterns = entry.getExclusionPatterns(); IPath[] inclusionPatterns = entry.getInclusionPatterns(); nextEntry: for (int i = 0; i < list.length; i++) { ILoadpathEntry other = list[i]; if (other.getEntryKind() == entry.getEntryKind() && other.isExported() == entry.isExported() && other.getPath().equals(entry.getPath())) { // check inclusion patterns IPath[] otherIncludes = other.getInclusionPatterns(); if (inclusionPatterns != otherIncludes) { if (inclusionPatterns == null) continue; int includeLength = inclusionPatterns.length; if (otherIncludes == null || otherIncludes.length != includeLength) continue; for (int j = 0; j < includeLength; j++) { // compare toStrings instead of IPaths // since IPath.equals is specified to ignore trailing separators if (!inclusionPatterns[j].toString().equals(otherIncludes[j].toString())) continue nextEntry; } } // check exclusion patterns IPath[] otherExcludes = other.getExclusionPatterns(); if (exclusionPatterns != otherExcludes) { if (exclusionPatterns == null) continue; int excludeLength = exclusionPatterns.length; if (otherExcludes == null || otherExcludes.length != excludeLength) continue; for (int j = 0; j < excludeLength; j++) { // compare toStrings instead of IPaths // since IPath.equals is specified to ignore trailing separators if (!exclusionPatterns[j].toString().equals(otherExcludes[j].toString())) continue nextEntry; } } return i; } } return -1; } /** * Recursively adds all subfolders of <code>folder</code> to the given collection. */ protected void collectAllSubfolders(IFolder folder, ArrayList collection) throws RubyModelException { try { IResource[] members= folder.members(); for (int i = 0, max = members.length; i < max; i++) { IResource r= members[i]; if (r.getType() == IResource.FOLDER) { collection.add(r); collectAllSubfolders((IFolder)r, collection); } } } catch (CoreException e) { throw new RubyModelException(e); } } /** * Returns a collection of package fragments that have been added/removed * as the result of changing the output location to/from the given * location. The collection is empty if no package fragments are * affected. */ protected ArrayList determineAffectedPackageFragments(IPath location) throws RubyModelException { ArrayList fragments = new ArrayList(); // see if this will cause any package fragments to be affected IWorkspace workspace = ResourcesPlugin.getWorkspace(); IResource resource = null; if (location != null) { resource = workspace.getRoot().findMember(location); } if (resource != null && resource.getType() == IResource.FOLDER) { IFolder folder = (IFolder) resource; // only changes if it actually existed ILoadpathEntry[] classpath = project.getExpandedLoadpath(true); for (int i = 0; i < classpath.length; i++) { ILoadpathEntry entry = classpath[i]; IPath path = classpath[i].getPath(); if (entry.getEntryKind() != ILoadpathEntry.CPE_PROJECT && path.isPrefixOf(location) && !path.equals(location)) { ISourceFolderRoot[] roots = project.computeSourceFolderRoots(classpath[i]); SourceFolderRoot root = (SourceFolderRoot) roots[0]; // now the output location becomes a package fragment - along with any subfolders ArrayList folders = new ArrayList(); folders.add(folder); collectAllSubfolders(folder, folders); Iterator elements = folders.iterator(); int segments = path.segmentCount(); while (elements.hasNext()) { IFolder f = (IFolder) elements.next(); IPath relativePath = f.getFullPath().removeFirstSegments(segments); String[] pkgName = relativePath.segments(); ISourceFolder pkg = root.getSourceFolder(pkgName); fragments.add(pkg); } } } } return fragments; } /** * Sets the classpath of the pre-specified project. */ protected void executeOperation() throws RubyModelException { // project reference updated - may throw an exception if unable to write .project file updateProjectReferencesIfNecessary(); // loadpath file updated - may throw an exception if unable to write .loadpath file saveLoadpathIfNecessary(); // perform classpath and output location updates, if exception occurs in loadpath update, // make sure the output location is updated before surfacing the exception (in case the output // location update also throws an exception, give priority to the loadpath update one). RubyModelException originalException = null; try { if (this.newRawPath == DO_NOT_UPDATE_PROJECT_REFS) this.newRawPath = project.getRawLoadpath(); if (this.newRawPath != DO_NOT_SET_ENTRIES){ updateLoadpath(); project.updateSourceFolderRoots(); RubyModelManager.getRubyModelManager().getDeltaProcessor().addForRefresh(project); } } catch(RubyModelException e){ originalException = e; throw e; } finally { // if traversed by an exception we still need to update the output location when necessary // ensures the project is getting rebuilt if only variable is modified if (!this.identicalRoots && this.canChangeResources) { try { this.project.getProject().touch(this.progressMonitor); } catch (CoreException e) { if (RubyModelManager.CP_RESOLVE_VERBOSE){ Util.verbose("CPContainer INIT - FAILED to touch project: "+ this.project.getElementName(), System.err); //$NON-NLS-1$ e.printStackTrace(); } } } } done(); } /** * Generates the delta of removed/added/reordered roots. * Use three deltas in case the same root is removed/added/reordered (for * instance, if it is changed from K_SOURCE to K_BINARY or vice versa) */ protected void generateLoadpathChangeDeltas() { RubyModelManager manager = RubyModelManager.getRubyModelManager(); if (manager.deltaState.findRubyProject(this.project.getElementName()) == null) // project doesn't exist yet (we're in an IWorkspaceRunnable) // no need to create a delta here and no need to index (see https://bugs.eclipse.org/bugs/show_bug.cgi?id=133334) // the delta processor will create an ADDED project delta, and index the project return; boolean needToUpdateDependents = false; RubyElementDelta delta = new RubyElementDelta(getRubyModel()); boolean hasDelta = false; if (this.loadpathWasSaved) { delta.changed(this.project, IRubyElementDelta.F_CLASSPATH_CHANGED); hasDelta = true; } int oldLength = oldResolvedPath.length; int newLength = newResolvedPath.length; final IndexManager indexManager = manager.getIndexManager(); Map oldRoots = null; ISourceFolderRoot[] roots = null; if (project.isOpen()) { try { roots = project.getSourceFolderRoots(); } catch (RubyModelException e) { // ignore } } else { Map allRemovedRoots ; if ((allRemovedRoots = manager.getDeltaProcessor().removedRoots) != null) { roots = (ISourceFolderRoot[]) allRemovedRoots.get(project); } } if (roots != null) { oldRoots = new HashMap(); for (int i = 0; i < roots.length; i++) { ISourceFolderRoot root = roots[i]; oldRoots.put(root.getPath(), root); } } for (int i = 0; i < oldLength; i++) { int index = classpathContains(newResolvedPath, oldResolvedPath[i]); if (index == -1) { // do not notify remote project changes if (oldResolvedPath[i].getEntryKind() == ILoadpathEntry.CPE_PROJECT){ needToUpdateDependents = true; this.needCycleCheck = true; continue; } ISourceFolderRoot[] pkgFragmentRoots = null; if (oldRoots != null) { ISourceFolderRoot oldRoot = (ISourceFolderRoot) oldRoots.get(oldResolvedPath[i].getPath()); if (oldRoot != null) { // use old root if any (could be none if entry wasn't bound) pkgFragmentRoots = new ISourceFolderRoot[] { oldRoot }; } } if (pkgFragmentRoots == null) { try { ObjectVector accumulatedRoots = new ObjectVector(); HashSet rootIDs = new HashSet(5); rootIDs.add(project.rootID()); project.computeSourceFolderRoots( oldResolvedPath[i], accumulatedRoots, rootIDs, null, // inside original project false, // don't check existency false, // don't retrieve exported roots null); /*no reverse map*/ pkgFragmentRoots = new ISourceFolderRoot[accumulatedRoots.size()]; accumulatedRoots.copyInto(pkgFragmentRoots); } catch (RubyModelException e) { pkgFragmentRoots = new ISourceFolderRoot[] {}; } } addLoadpathDeltas(pkgFragmentRoots, IRubyElementDelta.F_REMOVED_FROM_CLASSPATH, delta); int changeKind = oldResolvedPath[i].getEntryKind(); needToUpdateDependents |= (changeKind == ILoadpathEntry.CPE_SOURCE) || oldResolvedPath[i].isExported(); // Remove the .java files from the index for a source folder // For a lib folder or a .jar file, remove the corresponding index if not shared. if (indexManager != null) { ILoadpathEntry oldEntry = oldResolvedPath[i]; final IPath path = oldEntry.getPath(); switch (changeKind) { case ILoadpathEntry.CPE_SOURCE: final char[][] inclusionPatterns = ((LoadpathEntry)oldEntry).fullInclusionPatternChars(); final char[][] exclusionPatterns = ((LoadpathEntry)oldEntry).fullExclusionPatternChars(); postAction(new IPostAction() { public String getID() { return path.toString(); } public void run() /* throws RubyModelException */ { indexManager.removeSourceFolderFromIndex(project, path, inclusionPatterns, exclusionPatterns); } }, REMOVEALL_APPEND); break; case ILoadpathEntry.CPE_LIBRARY: final DeltaProcessingState deltaState = manager.deltaState; postAction(new IPostAction() { public String getID() { return path.toString(); } public void run() /* throws RubyModelException */ { if (deltaState.otherRoots.get(path) == null) { // if root was not shared indexManager.discardJobs(path.toString()); indexManager.removeIndex(path); // TODO (kent) we could just remove the in-memory index and have the indexing check for timestamps } } }, REMOVEALL_APPEND); break; } } hasDelta = true; } else { // do not notify remote project changes if (oldResolvedPath[i].getEntryKind() == ILoadpathEntry.CPE_PROJECT){ // Need to updated dependents in case old and/or new entries are exported and have an access restriction LoadpathEntry oldEntry = (LoadpathEntry) oldResolvedPath[i]; LoadpathEntry newEntry = (LoadpathEntry) newResolvedPath[index]; this.needCycleCheck |= (oldEntry.isExported() != newEntry.isExported()); continue; } needToUpdateDependents |= (oldResolvedPath[i].isExported() != newResolvedPath[index].isExported()); if (index != i) { //reordering of the classpath addLoadpathDeltas( project.computeSourceFolderRoots(oldResolvedPath[i]), IRubyElementDelta.F_REORDER, delta); int changeKind = oldResolvedPath[i].getEntryKind(); needToUpdateDependents |= (changeKind == ILoadpathEntry.CPE_SOURCE); hasDelta = true; } } } for (int i = 0; i < newLength; i++) { int index = classpathContains(oldResolvedPath, newResolvedPath[i]); if (index == -1) { // do not notify remote project changes if (newResolvedPath[i].getEntryKind() == ILoadpathEntry.CPE_PROJECT){ needToUpdateDependents = true; this.needCycleCheck = true; continue; } addLoadpathDeltas( project.computeSourceFolderRoots(newResolvedPath[i]), IRubyElementDelta.F_ADDED_TO_CLASSPATH, delta); int changeKind = newResolvedPath[i].getEntryKind(); // Request indexing if (indexManager != null) { switch (changeKind) { case ILoadpathEntry.CPE_LIBRARY: boolean pathHasChanged = true; final IPath newPath = newResolvedPath[i].getPath(); for (int j = 0; j < oldLength; j++) { ILoadpathEntry oldEntry = oldResolvedPath[j]; if (oldEntry.getPath().equals(newPath)) { pathHasChanged = false; break; } } if (pathHasChanged) { postAction(new IPostAction() { public String getID() { return newPath.toString(); } public void run() /* throws RubyModelException */ { indexManager.indexLibrary(newPath, project.getProject()); } }, REMOVEALL_APPEND); } break; case ILoadpathEntry.CPE_SOURCE: ILoadpathEntry entry = newResolvedPath[i]; final IPath path = entry.getPath(); final char[][] inclusionPatterns = ((LoadpathEntry)entry).fullInclusionPatternChars(); final char[][] exclusionPatterns = ((LoadpathEntry)entry).fullExclusionPatternChars(); postAction(new IPostAction() { public String getID() { return path.toString(); } public void run() /* throws RubyModelException */ { indexManager.indexSourceFolder(project, path, inclusionPatterns, exclusionPatterns); } }, APPEND); // append so that a removeSourceFolder action is not removed break; } } needToUpdateDependents |= (changeKind == ILoadpathEntry.CPE_SOURCE) || newResolvedPath[i].isExported(); hasDelta = true; } // classpath reordering has already been generated in previous loop } if (hasDelta) { this.addDelta(delta); } else { this.identicalRoots = true; } if (needToUpdateDependents){ updateAffectedProjects(project.getProject().getFullPath()); } } protected ISchedulingRule getSchedulingRule() { return null; // no lock taken while setting the classpath } /** * Returns <code>true</code> if this operation performs no resource modifications, * otherwise <code>false</code>. Subclasses must override. */ public boolean isReadOnly() { return !this.canChangeResources; } protected void saveLoadpathIfNecessary() throws RubyModelException { if (!this.canChangeResources || !this.needSave) return; ILoadpathEntry[] loadpathForSave; if (this.newRawPath == DO_NOT_SET_ENTRIES || this.newRawPath == DO_NOT_UPDATE_PROJECT_REFS){ loadpathForSave = project.getRawLoadpath(); } else { loadpathForSave = this.newRawPath; } // if read-only .loadpath, then the loadpath setting will never been performed completely if (project.saveLoadpath(loadpathForSave, null)) { this.loadpathWasSaved = true; this.setAttribute(HAS_MODIFIED_RESOURCE_ATTR, TRUE); } } public String toString(){ StringBuffer buffer = new StringBuffer(20); buffer.append("SetLoadpathOperation\n"); //$NON-NLS-1$ buffer.append(" - classpath : "); //$NON-NLS-1$ if (this.newRawPath == DO_NOT_SET_ENTRIES){ buffer.append("<Reuse Existing Loadpath Entries>"); //$NON-NLS-1$ } else { buffer.append("{"); //$NON-NLS-1$ for (int i = 0; i < this.newRawPath.length; i++) { if (i > 0) buffer.append(","); //$NON-NLS-1$ ILoadpathEntry element = this.newRawPath[i]; buffer.append(" ").append(element.toString()); //$NON-NLS-1$ } } buffer.append("\n - output location : "); //$NON-NLS-1$ if (this.newOutputLocation == DO_NOT_SET_OUTPUT){ buffer.append("<Reuse Existing Output Location>"); //$NON-NLS-1$ } else { buffer.append(this.newOutputLocation.toString()); } return buffer.toString(); } private void updateLoadpath() throws RubyModelException { beginTask(Messages.bind(Messages.classpath_settingProgress, project.getElementName()), 2); // SIDE-EFFECT: from thereon, the loadpath got modified project.getPerProjectInfo().updateLoadpathInformation(this.newRawPath); // resolve new path (asking for marker creation if problems) if (this.newResolvedPath == null) { this.newResolvedPath = project.getResolvedLoadpath(true, this.canChangeResources, false/*don't returnResolutionInProgress*/); } if (this.oldResolvedPath != null) { generateLoadpathChangeDeltas(); } else { this.needCycleCheck = true; updateAffectedProjects(project.getProject().getFullPath()); } updateCycleMarkersIfNecessary(); } /** * Update projects which are affected by this classpath change: * those which refers to the current project as source (indirectly) */ protected void updateAffectedProjects(IPath prerequisiteProjectPath) { // remove all update classpath post actions for this project final String updateLoadpath = "UpdateClassPath:"; //$NON-NLS-1$ removeAllPostAction(updateLoadpath + prerequisiteProjectPath.toString()); try { IRubyModel model = RubyModelManager.getRubyModelManager().getRubyModel(); IRubyProject initialProject = this.project; IRubyProject[] projects = model.getRubyProjects(); for (int i = 0, projectCount = projects.length; i < projectCount; i++) { try { final RubyProject affectedProject = (RubyProject) projects[i]; if (affectedProject.equals(initialProject)) continue; // skip itself if (!affectedProject.isOpen()) continue; // skip project as its namelookup caches do not exist // consider ALL dependents (even indirect ones), since they may need to // flush their respective namelookup caches (all pkg fragment roots). ILoadpathEntry[] classpath = affectedProject.getExpandedLoadpath(true); for (int j = 0, entryCount = classpath.length; j < entryCount; j++) { ILoadpathEntry entry = classpath[j]; if (entry.getEntryKind() == ILoadpathEntry.CPE_PROJECT && entry.getPath().equals(prerequisiteProjectPath)) { postAction(new IPostAction() { public String getID() { return updateLoadpath + affectedProject.getPath().toString(); } public void run() throws RubyModelException { affectedProject.setRawLoadpath( DO_NOT_UPDATE_PROJECT_REFS, SetLoadpathOperation.DO_NOT_SET_OUTPUT, SetLoadpathOperation.this.progressMonitor, SetLoadpathOperation.this.canChangeResources, affectedProject.getResolvedLoadpath(true/*ignoreUnresolvedEntry*/, false/*don't generateMarkerOnError*/, false/*don't returnResolutionInProgress*/), false, // updating only - no validation false); // updating only - no need to save } }, REMOVEALL_APPEND); break; } } } catch (RubyModelException e) { // ignore } } } catch (RubyModelException e) { // ignore } } /** * Update cycle markers */ protected void updateCycleMarkersIfNecessary() { if (!this.needCycleCheck) return; if (!this.canChangeResources) return; if (!project.hasCycleMarker() && !project.hasLoadpathCycle(newResolvedPath)){ return; } postAction( new IPostAction() { public String getID() { return "updateCycleMarkers"; //$NON-NLS-1$ } public void run() throws RubyModelException { RubyProject.updateAllCycleMarkers(null); } }, REMOVEALL_APPEND); } /** * Update projects references so that the build order is consistent with the classpath */ protected void updateProjectReferencesIfNecessary() throws RubyModelException { if (this.newRawPath == DO_NOT_SET_ENTRIES || this.newRawPath == DO_NOT_UPDATE_PROJECT_REFS) return; // will run now, or be deferred until next pre-auto-build notification if resource tree is locked RubyModelManager.getRubyModelManager().deltaState.updateProjectReferences( project, oldResolvedPath, newResolvedPath, newRawPath, canChangeResources); } public IRubyModelStatus verify() { IRubyModelStatus status = super.verify(); if (!status.isOK()) { return status; } if (needValidation) { // retrieve classpath ILoadpathEntry[] entries = this.newRawPath; if (entries == DO_NOT_SET_ENTRIES){ try { entries = project.getRawLoadpath(); } catch (RubyModelException e) { return e.getRubyModelStatus(); } } // perform validation return LoadpathEntry.validateLoadpath( project, entries, null); } return RubyModelStatus.VERIFIED_OK; } }