/******************************************************************************* * Copyright (c) 2006, 2013 Oracle. 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: * Oracle - initial API and implementation ******************************************************************************/ package org.eclipse.jpt.jpa.core.internal; import java.util.HashSet; import java.util.Vector; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; 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.IWorkspace; import org.eclipse.core.resources.IWorkspaceRoot; import org.eclipse.core.resources.IncrementalProjectBuilder; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.NullProgressMonitor; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobManager; import org.eclipse.core.runtime.jobs.ISchedulingRule; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.jdt.core.ElementChangedEvent; import org.eclipse.jdt.core.IElementChangedListener; import org.eclipse.jdt.core.IJavaElement; import org.eclipse.jdt.core.IJavaElementDelta; import org.eclipse.jdt.core.JavaCore; import org.eclipse.jpt.common.core.JptCommonCoreMessages; import org.eclipse.jpt.common.core.internal.utility.ProjectTools; import org.eclipse.jpt.common.core.internal.utility.ResourceChangeAdapter; import org.eclipse.jpt.common.core.internal.utility.command.CommandJobCommandAdapter; import org.eclipse.jpt.common.core.internal.utility.command.InactiveExtendedJobCommandContext; import org.eclipse.jpt.common.core.internal.utility.command.JobCommandAdapter; import org.eclipse.jpt.common.core.internal.utility.command.SimpleJobCommandContext; import org.eclipse.jpt.common.core.internal.utility.command.SingleUseQueueingExtendedJobCommandContext; import org.eclipse.jpt.common.core.utility.command.ExtendedJobCommandContext; import org.eclipse.jpt.common.core.utility.command.JobCommand; import org.eclipse.jpt.common.utility.command.Command; import org.eclipse.jpt.common.utility.command.ExtendedCommandContext; import org.eclipse.jpt.common.utility.internal.ObjectTools; import org.eclipse.jpt.common.utility.internal.command.CommandAdapter; import org.eclipse.jpt.common.utility.internal.command.NullCommand; import org.eclipse.jpt.common.utility.internal.command.ThreadLocalExtendedCommandContext; import org.eclipse.jpt.common.utility.internal.iterable.EmptyIterable; import org.eclipse.jpt.common.utility.internal.iterable.IterableTools; import org.eclipse.jpt.common.utility.internal.iterable.SingleElementIterable; import org.eclipse.jpt.common.utility.internal.model.AbstractModel; import org.eclipse.jpt.common.utility.internal.reference.FalseBooleanReference; import org.eclipse.jpt.common.utility.reference.BooleanReference; import org.eclipse.jpt.common.utility.reference.ModifiableObjectReference; import org.eclipse.jpt.jpa.core.JpaPlatform; import org.eclipse.jpt.jpa.core.JpaPreferences; import org.eclipse.jpt.jpa.core.JpaProject; import org.eclipse.jpt.jpa.core.JpaProjectManager; import org.eclipse.jpt.jpa.core.JpaWorkspace; import org.eclipse.jpt.jpa.core.JptJpaCoreMessages; import org.eclipse.jpt.jpa.core.internal.plugin.JptJpaCorePlugin; import org.eclipse.jpt.common.core.internal.utility.ValidationMessageTools; import org.eclipse.jpt.jpa.core.platform.JpaPlatformManager; import org.eclipse.jpt.jpa.core.validation.JptJpaCoreValidationMessages; import org.eclipse.osgi.util.NLS; import org.eclipse.wst.common.project.facet.core.IProjectFacetVersion; import org.eclipse.wst.common.project.facet.core.ProjectFacetsManager; import org.eclipse.wst.validation.internal.provisional.core.IMessage; import org.eclipse.wst.validation.internal.provisional.core.IReporter; /** * The JPA project manager maintains a list of all the JPA projects in a * workspace. It keeps the list (and the state of the JPA projects themselves) * synchronized with the workspace by listening for Resource and Java change * events. *

* We use Eclipse {@link ISchedulingRule scheduling rules} to synchronize * access to the JPA projects when dealing with these events. In an effort to * reduce deadlocks, the Resource and Java change events are dispatched to * background jobs, allowing us to handle the events outside of the workspace * lock held during Resource and Java change notifications. The * {@link ISchedulingRule scheduling rules} are also used to synchronize the * event handling with the various other asynchronous Dali activities:

* Any method that returns a value (e.g. {@link #waitToGetJpaProjects()}) is * "synchronized" with the background jobs. This allows any outstanding events * to be handled before the value is returned. *

* Various things that cause us to add or remove a JPA project: *

*/ class InternalJpaProjectManager extends AbstractModel implements JpaProject.Manager { /** * The JPA workspace the JPA project manager corresponds to. */ private final JpaWorkspace jpaWorkspace; /** * Determine how commands (Resource and Java change events etc.) are * handled (i.e. synchronously or asynchronously). * The default command context executes commands asynchronously via * Eclipse {@link Job jobs}. */ private volatile ExtendedJobCommandContext commandContext; /** * All the JPA projects in the workspace. */ private final Vector jpaProjects = new Vector(); /** * Listen for */ private final IResourceChangeListener resourceChangeListener = new ResourceChangeListener(); /** * The types of resource change events that interest * {@link #resourceChangeListener}. */ private static final int RESOURCE_CHANGE_EVENT_TYPES = IResourceChangeEvent.POST_CHANGE | IResourceChangeEvent.POST_BUILD; /** * Listen for Java changes (unless the Dali UI is active). * @see #javaEventListenersAreActive() */ private final JavaElementChangeListener javaElementChangeListener = new JavaElementChangeListener(); /** * The types of Java element change events that interest * {@link #javaElementChangeListener}. */ private static final int JAVA_CHANGE_EVENT_TYPES = ElementChangedEvent.POST_CHANGE | ElementChangedEvent.POST_RECONCILE; /** * A set of flags to activate/deactivate the java event listener. *

* This set of flags hacks around our problems with bi-directional updates. * We must worry about changes to the Java source code that are * initiated by the Dali resource model because there is no way to either * receive the corresponding Java events synchronously or suppress * them. We need not worry about changes initiated by the Java * source code since they are handled by the Java resource model in a way * that does not return any changes back to the Java source code. *

* We can ignore Java events whenever Dali is manipulating the Java * source code via the Dali model. We do this because the 0.5 sec delay * between the Java source being changed in the Java Editor and the * corresponding event being fired causes us no end of pain.... *

* NB: The following may not be accurate in Eclipse 4.x.... *

* Fortunately we will be active whenever a Java source file is * saved, even when is pressed from within a Dali composite. This * is because, when the file is saved, the editor, presumably, * takes the focus temporarily, activating this listener. Then, while this * listener is active, we receive a Java change event for the compilation * unit being saved. This allows us to re-cache our resource model text * ranges so they are in sync with the Java source before execution of the * validation job. Once the Java source file is saved, the Dali composite * re-takes the focus and this listener is once again inactive. */ private final HashSet javaEventListenerFlags = new HashSet(); /** * Support for modifying documents shared with the UI. */ private final ThreadLocalExtendedCommandContext modifySharedDocumentCommandContext = new ThreadLocalExtendedCommandContext(); // ********** constructor ********** /** * Internal: Called only by the * {@link InternalJpaWorkspace#buildJpaProjectManager() JPA workspace}. *

* NB: The JPA projects are built asynchronously. */ InternalJpaProjectManager(JpaWorkspace jpaWorkspace) { super(); this.jpaWorkspace = jpaWorkspace; // dump a stack trace so we can determine what triggers this JptJpaCorePlugin.instance().dumpStackTrace(TRACE_OPTION, "*** new JPA project manager ***"); //$NON-NLS-1$ try { // 406353 - prevent scheduling conflicts by forcing initialization // of the Java plug-in *before* we begin building JPA projects; // otherwise, it could be lazily initialized as we build JPA projects... // [that's the hope - we have not been able to re-create and test the bug...] JavaCore.initializeAfterLoad(null); this.commandContext = this.buildAsynchronousCommandContext(); this.buildJpaProjects(); // typically async this.getWorkspace().addResourceChangeListener(this.resourceChangeListener, RESOURCE_CHANGE_EVENT_TYPES); JavaCore.addElementChangedListener(this.javaElementChangeListener, JAVA_CHANGE_EVENT_TYPES); } catch (Exception ex) { JptJpaCorePlugin.instance().logError(ex); this.dispose(); } } // ********** build JPA projects ********** /** * The JPA projects are built asynchronously in a set of jobs, one for each * project in the workspace that has the JPA facet. * Side-effect: {@link #jpaProjects} populated. */ private void buildJpaProjects() { for (IProject project : this.getWorkspaceRoot().getProjects()) { if (ProjectTools.hasFacet(project, JpaProject.FACET)) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "dispatch: build JPA project: {0}", project.getName()); //$NON-NLS-1$ BuildJpaProjectCommand command = new BuildJpaProjectCommand(project); this.execute(command, NLS.bind(JptJpaCoreMessages.BUILD_JPA_PROJECT_JOB_NAME, project.getName()), project); } } } /* CU private */ class BuildJpaProjectCommand extends CommandAdapter { private final IProject project; BuildJpaProjectCommand(IProject project) { super(); this.project = project; } @Override public void execute() { InternalJpaProjectManager.this.buildJpaProject_(this.project); } } /* CU private */ void buildJpaProject_(IProject project) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "execute: build JPA project: {0}", project.getName()); //$NON-NLS-1$ this.addJpaProject(project); } // ********** disposal ********** /** * Internal: Called only by the * {@link InternalJpaWorkspace#dispose() JPA workspace}. * Once disposed, the JPA project manager cannot be restarted. */ void dispose() { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "*** JPA project manager dispose ***"); //$NON-NLS-1$ JavaCore.removeElementChangedListener(this.javaElementChangeListener); this.getWorkspace().removeResourceChangeListener(this.resourceChangeListener); ExtendedJobCommandContext oldCE = this.commandContext; // if the current context is async, commands can continue to execute after we replace it here... this.commandContext = InactiveExtendedJobCommandContext.instance(); this.clearJpaProjects(oldCE); // synchronous JptJpaCorePlugin.instance().trace(TRACE_OPTION, "*** JPA project manager DEAD ***"); //$NON-NLS-1$ } /** * Clear the JPA projects synchronously; * suspending the workspace shutdown until the currently executing Dali * jobs finish executing. *

* A typical scenario for outstanding Dali jobs is when the user saves a JPA * file (e.g. an JPA-annotated Java file) as the workspace shuts down. This * will trigger a validation job once the file is saved. */ private void clearJpaProjects(ExtendedJobCommandContext oldCE) { try { this.clearJpaProjects_(oldCE); } catch (InterruptedException ex) { // it would be interesting to know how we could get here... Thread.currentThread().interrupt(); JptJpaCorePlugin.instance().logError(ex); } } private void clearJpaProjects_(ExtendedJobCommandContext oldCE) throws InterruptedException { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "dispatch: clear JPA projects"); //$NON-NLS-1$ ClearJpaProjectsCommand command = new ClearJpaProjectsCommand(); oldCE.waitToExecute(command, JptJpaCoreMessages.DISPOSE_JPA_PROJECTS_JOB_NAME, this.getWorkspaceRoot()); JptJpaCorePlugin.instance().trace(TRACE_OPTION, "end: clear JPA projects"); //$NON-NLS-1$ } /* CU private */ class ClearJpaProjectsCommand extends JobCommandAdapter { @Override public IStatus execute(IProgressMonitor monitor) { InternalJpaProjectManager.this.clearJpaProjects_(monitor); return Status.OK_STATUS; } } /* CU private */ void clearJpaProjects_(IProgressMonitor monitor) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "execute: clear JPA projects"); //$NON-NLS-1$ // clone the collection to prevent concurrent modification exception for (JpaProject jpaProject : this.getJpaProjects()) { if (monitor.isCanceled()) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "CANCEL: clear JPA projects: {0}", jpaProject.getName()); //$NON-NLS-1$ throw new OperationCanceledException(); } // *remove* the JPA projects so we fire the appropriate model events(?) this.removeJpaProject(jpaProject); } JptJpaCorePlugin.instance().trace(TRACE_OPTION, "end: clear JPA projects"); //$NON-NLS-1$ } // ********** get JPA projects ********** public Iterable waitToGetJpaProjects() throws InterruptedException { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "dispatch: get JPA projects"); //$NON-NLS-1$ GetJpaProjectsCommand command = new GetJpaProjectsCommand(); this.waitToExecute(command, JptJpaCoreMessages.GET_JPA_PROJECTS_JOB_NAME, this.getWorkspaceRoot()); return command.result; } /* CU private */ class GetJpaProjectsCommand extends CommandAdapter { Iterable result; @Override public void execute() { this.result = InternalJpaProjectManager.this.getJpaProjects_(); } } /** * @see GetJpaProjectsCommand#execute() * @see ProjectAdapterFactory#getJpaProject(IProject) */ Iterable getJpaProjects_() { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "execute: get JPA projects: {0}", this.jpaProjects); //$NON-NLS-1$ // clone the JPA projects immediately, while we have the lock return this.getJpaProjects(); } public Iterable getJpaProjects() { return IterableTools.cloneSnapshot(this.jpaProjects); } public int getJpaProjectsSize() { return this.jpaProjects.size(); } // ********** get JPA project ********** /** * @see ProjectAdapterFactory.JpaProjectReference#getValue() */ JpaProject waitToGetJpaProject(IProject project) throws InterruptedException { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "dispatch: get JPA project: {0}", project.getName()); //$NON-NLS-1$ GetJpaProjectCommand command = new GetJpaProjectCommand(project); this.waitToExecute(command, NLS.bind(JptJpaCoreMessages.GET_JPA_PROJECT_JOB_NAME, project.getName()), project); return command.result; } /** * Not needed...? * It's nigh pointless to put a time-out on this call, since there are many * things that can trigger a time-out (e.g. validation). Either the client * wants to wait for the JPA project and uses a * {@link org.eclipse.jpt.jpa.core.JpaProject.Reference} * or the client can use the event notification mechanism to be notified * when the JPA project shows up.... */ // @see ProjectAdapterFactory.JpaProjectReference#getValue(ModifiableObjectReference, long) @SuppressWarnings("unused") private boolean waitToGetJpaProject(ModifiableObjectReference jpaProjectRef, IProject project, long timeout) throws InterruptedException { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "dispatch: get JPA project (time-out): {0}", project.getName()); //$NON-NLS-1$ GetJpaProjectCommand command = new GetJpaProjectCommand(project); boolean result = this.waitToExecute(command, JptJpaCoreMessages.GET_JPA_PROJECT_JOB_NAME, project, timeout); if (result) { jpaProjectRef.setValue(command.result); } else { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "time-out: get JPA project: {0}", project.getName()); //$NON-NLS-1$ } return result; } /* CU private */ class GetJpaProjectCommand extends CommandAdapter { private final IProject project; JpaProject result; GetJpaProjectCommand(IProject project) { super(); this.project = project; } @Override public void execute() { this.result = InternalJpaProjectManager.this.getJpaProjectUnsafe(this.project); } } /** * Pre-condition: called from {@link GetJpaProjectCommand#execute()} */ /* CU private */ JpaProject getJpaProjectUnsafe(IProject project) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "execute: get JPA project: {0}", project.getName()); //$NON-NLS-1$ // no need to clone here, since we have the lock JpaProject jpaProject = selectJpaProject(this.jpaProjects, project); if (jpaProject == null) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "not found: get JPA project: {0}", project.getName()); //$NON-NLS-1$ } return jpaProject; } private JpaProject getJpaProject_(IProject project) { return selectJpaProject(this.getJpaProjects(), project); } private static JpaProject selectJpaProject(Iterable jpaProjects, IProject project) { for (JpaProject jpaProject : jpaProjects) { if (jpaProject.getProject().equals(project)) { return jpaProject; } } return null; } // ********** rebuild JPA project ********** /** * @see ProjectAdapterFactory.JpaProjectReference#rebuild() */ // e.g. changed JPA platform JpaProject rebuildJpaProject(IProject project) throws InterruptedException { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "dispatch: rebuild JPA project: {0}", project.getName()); //$NON-NLS-1$ RebuildJpaProjectCommand command = new RebuildJpaProjectCommand(project); this.waitToExecute(command, NLS.bind(JptJpaCoreMessages.REBUILD_JPA_PROJECT_JOB_NAME, project.getName()), project); return command.result; } /* CU private */ class RebuildJpaProjectCommand extends JobCommandAdapter { private final IProject project; JpaProject result; RebuildJpaProjectCommand(IProject project) { super(); this.project = project; } @Override public IStatus execute(IProgressMonitor monitor) { this.result = InternalJpaProjectManager.this.rebuildJpaProject_(this.project, monitor); return Status.OK_STATUS; } } /* CU private */ JpaProject rebuildJpaProject_(IProject project, IProgressMonitor monitor) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "execute: rebuild JPA project: {0}", project.getName()); //$NON-NLS-1$ this.removeJpaProject(this.getJpaProject_(project)); if (monitor.isCanceled()) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "CANCEL: rebuild JPA project: {0}", project.getName()); //$NON-NLS-1$ throw new OperationCanceledException(); } return this.addJpaProject(project); } // ********** build validation messages ********** /** * @see ProjectAdapterFactory.JpaProjectReference#buildValidationMessages(IReporter) */ Iterable buildValidationMessages(IProject project, IReporter reporter) throws InterruptedException { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "dispatch: build validation messages: {0}", project.getName()); //$NON-NLS-1$ BuildValidationMessagesCommand command = new BuildValidationMessagesCommand(project, reporter); this.waitToExecute(command, NLS.bind(JptJpaCoreMessages.BUILD_VALIDATION_MESSAGES_JOB_NAME, project.getName()), project); return command.result; } /* CU private */ class BuildValidationMessagesCommand extends CommandAdapter { private final IProject project; private final IReporter reporter; Iterable result; BuildValidationMessagesCommand(IProject project, IReporter reporter) { super(); this.project = project; this.reporter = reporter; } @Override public void execute() { this.result = InternalJpaProjectManager.this.buildValidationMessages_(this.project, this.reporter); } } /* CU private */ Iterable buildValidationMessages_(IProject project, IReporter reporter) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "execute: build validation messages: {0}", project.getName()); //$NON-NLS-1$ JpaProject jpaProject = this.getJpaProject_(project); if (jpaProject == null) { return this.buildNoJpaProjectMessages(project); } try { // TODO catch exceptions further down too - so we get at least some msgs return jpaProject.getValidationMessages(reporter); } catch (RuntimeException ex) { JptJpaCorePlugin.instance().logError(ex); return EmptyIterable.instance(); } } private Iterable buildNoJpaProjectMessages(IProject project) { return new SingleElementIterable(this.buildNoJpaProjectMessage(project)); } private IMessage buildNoJpaProjectMessage(IProject project) { return ValidationMessageTools.buildValidationMessage(project, JptJpaCoreValidationMessages.NO_JPA_PROJECT); } // ********** add JPA project ********** /** * Pre-condition: The specified project is locked. * If there are any problems building the JPA project, * log the exception and do not add anything to the manager. * Return the newly-created JPA project. */ /* CU private */ JpaProject addJpaProject(IProject project) { JpaProject jpaProject = this.buildJpaProject(project); // dump a stack trace so we can determine what triggers this if (jpaProject == null) { JptJpaCorePlugin.instance().dumpStackTrace(TRACE_OPTION, "add JPA project fail: {0}", project); //$NON-NLS-1$ } else { JptJpaCorePlugin.instance().dumpStackTrace(TRACE_OPTION, "add JPA project: {0}", jpaProject); //$NON-NLS-1$ } // the JPA project will be null if we have any problems building it... // (e.g. if we have problems getting the JPA platform) if (jpaProject != null) { this.addItemToCollection(jpaProject, this.jpaProjects, JPA_PROJECTS_COLLECTION); } return jpaProject; } /** * Return null if we have any problems.... */ private JpaProject buildJpaProject(IProject project) { return this.buildJpaProject(this.buildJpaProjectConfig(project)); } /** * Return null if we have any problems.... */ private JpaProject buildJpaProject(JpaProject.Config config) { return this.buildJpaProject(config.getJpaPlatform(), config); } /** * Return null if we have any problems.... */ private JpaProject buildJpaProject(JpaPlatform jpaPlatform, JpaProject.Config config) { if (jpaPlatform == null) { JptJpaCorePlugin.instance().logError(new IllegalArgumentException(), "null JPA platform: {0}", config.getProject()); //$NON-NLS-1$ return null; } try { return jpaPlatform.getJpaFactory().buildJpaProject(config); } catch (RuntimeException ex) { JptJpaCorePlugin.instance().logError(ex); return null; } } private JpaProject.Config buildJpaProjectConfig(IProject project) { SimpleJpaProjectConfig config = new SimpleJpaProjectConfig(); config.setJpaProjectManager(this); config.setProject(project); config.setJpaPlatform(this.getJpaPlatform(project)); config.setConnectionProfileName(JpaPreferences.getConnectionProfileName(project)); config.setUserOverrideDefaultCatalog(JpaPreferences.getUserOverrideDefaultCatalog(project)); config.setUserOverrideDefaultSchema(JpaPreferences.getUserOverrideDefaultSchema(project)); config.setDiscoverAnnotatedClasses(JpaPreferences.getDiscoverAnnotatedClasses(project)); config.setMetamodelSourceFolderName(JpaPreferences.getMetamodelSourceFolderName(project)); return config; } private JpaPlatform getJpaPlatform(IProject project) { JpaPlatformManager mgr = this.jpaWorkspace.getJpaPlatformManager(); String jpaPlatformID = JpaPreferences.getJpaPlatformID(project); if (jpaPlatformID != null) { return mgr.getJpaPlatform(jpaPlatformID); } // the ID taken from the JPA preferences can be null if the JPA facet is // added by editing the facet metadata file directly and there are no // pre-existing Dali project preferences in the project .settings directory jpaPlatformID = this.getDefaultJpaPlatformID(project); // it's unlikely the default ID will be null... return (jpaPlatformID == null) ? null : mgr.getJpaPlatform(jpaPlatformID); } /** * Return the default JPA platform ID for the specified project. * This will be determined by the current default JPA platform for the * project's JPA facet version. */ private String getDefaultJpaPlatformID(IProject project) { IProjectFacetVersion jpaFacetVersion = this.getJpaFacetVersion(project); if (jpaFacetVersion == null) { return null; } JpaPlatform.Config config = this.jpaWorkspace.getJpaPlatformManager().getDefaultJpaPlatformConfig(jpaFacetVersion); return (config == null) ? null : config.getId(); } private IProjectFacetVersion getJpaFacetVersion(IProject project) { try { return ProjectFacetsManager.create(project).getProjectFacetVersion(JpaProject.FACET); } catch (CoreException ex) { JptJpaCorePlugin.instance().logError(ex); return null; } } // ********** remove JPA project ********** /** * Pre-condition: The specified JPA project's project is locked. */ private void removeJpaProject(JpaProject jpaProject) { // dump a stack trace so we can determine what triggers this JptJpaCorePlugin.instance().dumpStackTrace(TRACE_OPTION, "remove JPA project: {0}", jpaProject); //$NON-NLS-1$ this.removeItemFromCollection(jpaProject, this.jpaProjects, JPA_PROJECTS_COLLECTION); this.disposeJpaProject(jpaProject); } private void disposeJpaProject(JpaProject jpaProject) { try { jpaProject.dispose(); } catch (RuntimeException ex) { JptJpaCorePlugin.instance().logError(ex); } } // ********** Project POST_CHANGE ********** /* CU private */ void projectChanged(IResourceDelta delta) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "dispatch: project changed: {0}", delta.getResource()); //$NON-NLS-1$ ProjectChangeEventHandlerCommand command = new ProjectChangeEventHandlerCommand(delta); this.execute(command, JptJpaCoreMessages.PROJECT_CHANGE_EVENT_HANDLER_JOB_NAME, this.getWorkspaceRoot()); } /* CU private */ class ProjectChangeEventHandlerCommand extends JobCommandAdapter { private final IResourceDelta delta; ProjectChangeEventHandlerCommand(IResourceDelta delta) { super(); this.delta = delta; } @Override public IStatus execute(IProgressMonitor monitor) { InternalJpaProjectManager.this.projectChanged_(this.delta, monitor); return Status.OK_STATUS; } } /** * Forward the specified resource delta to all the JPA projects; * they will each determine whether the event is significant. */ /* CU private */ void projectChanged_(IResourceDelta delta, IProgressMonitor monitor) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "execute: project changed: {0}", delta.getResource()); //$NON-NLS-1$ // debug("execute: project changed: ", ((org.eclipse.core.internal.events.ResourceDelta) delta).toDeepDebugString()); //$NON-NLS-1$ for (JpaProject jpaProject : this.jpaProjects) { if (monitor.isCanceled()) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "CANCEL: project changed: {0}", jpaProject.getName()); //$NON-NLS-1$ throw new OperationCanceledException(); } try { jpaProject.projectChanged(delta); } catch (RuntimeException ex) { JptJpaCorePlugin.instance().logError(ex); } } } // ********** Project POST_BUILD (CLEAN_BUILD) ********** /* CU private */ void projectPostCleanBuild(IProject project) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "dispatch: post clean build: {0}", project.getName()); //$NON-NLS-1$ ProjectPostCleanBuildEventHandlerCommand command = new ProjectPostCleanBuildEventHandlerCommand(project); this.execute(command, NLS.bind(JptJpaCoreMessages.PROJECT_POST_CLEAN_BUILD_EVENT_HANDLER_JOB_NAME, project.getName()), project); } /* CU private */ class ProjectPostCleanBuildEventHandlerCommand extends JobCommandAdapter { private final IProject project; ProjectPostCleanBuildEventHandlerCommand(IProject project) { super(); this.project = project; } @Override public IStatus execute(IProgressMonitor monitor) { InternalJpaProjectManager.this.projectPostCleanBuild_(this.project, monitor); return Status.OK_STATUS; } } /* CU private */ void projectPostCleanBuild_(IProject project, IProgressMonitor monitor) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "execute: post clean build: {0}", project.getName()); //$NON-NLS-1$ JpaProject jpaProject = this.getJpaProject_(project); if (jpaProject != null) { this.removeJpaProject(jpaProject); if (monitor.isCanceled()) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "CANCEL: post clean build: {0}", project.getName()); //$NON-NLS-1$ throw new OperationCanceledException(); } this.addJpaProject(project); } } // ********** File POST_CHANGE ********** /** * The Faceted Project settings file has changed in some fashion, check * whether the JPA facet has been added to or removed from the specified * project. */ /* CU private */ void checkForJpaFacetTransition(IProject project) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "dispatch: project facet file changed: {0}", project.getName()); //$NON-NLS-1$ FacetFileChangeEventHandlerCommand command = new FacetFileChangeEventHandlerCommand(project); this.execute(command, NLS.bind(JptJpaCoreMessages.FACET_FILE_CHANGE_EVENT_HANDLER_JOB_NAME, project.getName()), project); } /* CU private */ class FacetFileChangeEventHandlerCommand extends CommandAdapter { private final IProject project; FacetFileChangeEventHandlerCommand(IProject project) { super(); this.project = project; } @Override public void execute() { InternalJpaProjectManager.this.checkForJpaFacetTransition_(this.project); } } /* CU private */ void checkForJpaFacetTransition_(IProject project) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "execute: project facet file changed: {0}", project.getName()); //$NON-NLS-1$ JpaProject jpaProject = this.getJpaProject_(project); if (ProjectTools.hasFacet(project, JpaProject.FACET)) { if (jpaProject == null) { // JPA facet added this.addJpaProject(project); } } else { if (jpaProject != null) { // JPA facet removed this.removeJpaProject(jpaProject); } } } // ********** Java element changed ********** /* CU private */ void javaElementChanged(ElementChangedEvent event) { if (this.handleJavaElementChangedEvent(event)) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "dispatch: Java element changed: {0}", event.getDelta()); //$NON-NLS-1$ JavaChangeEventHandlerCommand command = new JavaChangeEventHandlerCommand(event); this.execute(command, JptJpaCoreMessages.JAVA_CHANGE_EVENT_HANDLER_JOB_NAME, this.getWorkspaceRoot()); } } /* CU private */ class JavaChangeEventHandlerCommand extends JobCommandAdapter { private final ElementChangedEvent event; JavaChangeEventHandlerCommand(ElementChangedEvent event) { super(); this.event = event; } @Override public IStatus execute(IProgressMonitor monitor) { InternalJpaProjectManager.this.javaElementChanged_(this.event, monitor); return Status.OK_STATUS; } } /** * Forward the Java element changed event to all the JPA projects * as the event could affect multiple projects. */ /* CU private */ void javaElementChanged_(ElementChangedEvent event, IProgressMonitor monitor) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "execute: Java element changed: {0}", event.getDelta()); //$NON-NLS-1$ for (JpaProject jpaProject : this.jpaProjects) { if (monitor.isCanceled()) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "CANCEL: Java element changed: {0}", jpaProject.getName()); //$NON-NLS-1$ throw new OperationCanceledException(); } try { jpaProject.javaElementChanged(event); } catch (RuntimeException ex) { JptJpaCorePlugin.instance().logError(ex); } } } /** * Check to see if we should even handle this particular java event. If it * needs to be handled then we will kick off our 'JPA Java Change Event Handler' job. * If any change in the event needs to be handled, we short-circuit out and * return true. Each JpaProject then makes the same checks to determine * which changes are of concern. We are trying to limit the number * of 'JPA Java Change Event Handler' jobs that are run. *
* This code was copied and modified from AbstractJpaProject, so make sure * to make changes in both locations. * * @see AbstractJpaProject#processJavaDelta(IJavaElementDelta) */ private boolean handleJavaElementChangedEvent(ElementChangedEvent event) { return this.handleJavaDelta(event.getDelta()); } /** * We recurse back here from {@link #handleJavaDeltaChildren(IJavaElementDelta)}. */ protected boolean handleJavaDelta(IJavaElementDelta delta) { switch (delta.getElement().getElementType()) { case IJavaElement.JAVA_MODEL : return this.handleJavaModelDelta(delta); case IJavaElement.JAVA_PROJECT : return this.handleJavaProjectDelta(delta); case IJavaElement.PACKAGE_FRAGMENT_ROOT : return this.handleJavaPackageFragmentRootDelta(delta); case IJavaElement.PACKAGE_FRAGMENT : return this.processJavaPackageFragmentDelta(delta); case IJavaElement.COMPILATION_UNIT : return this.handleJavaCompilationUnitDelta(delta); default : break; // ignore the elements inside a compilation unit } return false; } protected boolean handleJavaDeltaChildren(IJavaElementDelta delta) { for (IJavaElementDelta child : delta.getAffectedChildren()) { if (this.handleJavaDelta(child)) { // recurse return true; } } return false; } // ***** model protected boolean handleJavaModelDelta(IJavaElementDelta delta) { // process the Java model's projects return this.handleJavaDeltaChildren(delta); } // ***** project protected boolean handleJavaProjectDelta(IJavaElementDelta delta) { // process the Java project's package fragment roots boolean handle = this.handleJavaDeltaChildren(delta); if (handle) { //if any child needs to be handled, we're done return handle; } // a classpath change can have pretty far-reaching effects... if (AbstractJpaProject.classpathHasChanged(delta)) { return true; } return false; } // ***** package fragment root protected boolean handleJavaPackageFragmentRootDelta(IJavaElementDelta delta) { // process the Java package fragment root's package fragments boolean handle = this.handleJavaDeltaChildren(delta); if (handle) { //if any child needs to be handled, we're done return handle; } if (AbstractJpaProject.classpathEntryHasBeenAdded(delta)) { return true; // TODO bug 277218 } else if (AbstractJpaProject.classpathEntryHasBeenRemoved(delta)) { // should be mutually-exclusive w/added (?) return true; // TODO bug 277218 } return false; } // ***** package fragment protected boolean processJavaPackageFragmentDelta(IJavaElementDelta delta) { // process the java package fragment's compilation units return this.handleJavaDeltaChildren(delta); } // ***** compilation unit protected boolean handleJavaCompilationUnitDelta(IJavaElementDelta delta) { if (this.javaCompilationUnitDeltaIsRelevant(delta)) { return true; } // ignore the java compilation unit's children return false; } protected boolean javaCompilationUnitDeltaIsRelevant(IJavaElementDelta delta) { return AbstractJpaProject.javaCompilationUnitDeltaIsRelevant(delta); } // ********** support for modifying documents shared with the UI ********** public ExtendedCommandContext getModifySharedDocumentCommandContext() { return this.modifySharedDocumentCommandContext; } private void setThreadLocalModifySharedDocumentCommandContext(ExtendedCommandContext commandContext) { this.modifySharedDocumentCommandContext.set(commandContext); } // ********** misc ********** public JpaWorkspace getJpaWorkspace() { return this.jpaWorkspace; } private IWorkspace getWorkspace() { return this.jpaWorkspace.getWorkspace(); } private IWorkspaceRoot getWorkspaceRoot() { return this.getWorkspace().getRoot(); } private ISchedulingRule getCurrentRule() { return this.getJobManager().currentRule(); } private IJobManager getJobManager() { return Job.getJobManager(); } @Override public void toString(StringBuilder sb) { sb.append(this.getJpaProjects()); } // ********** command execution ********** /** * Make sure the JPA project is still around when the command reaches the * front of the command queue. *

* Called by JPA project synchronizes and updates: * @see org.eclipse.jpt.jpa.core.internal.AbstractJpaProject.ManagerJobCommandContext#execute(JobCommand, String) */ public void execute(JobCommand command, String jobName, JpaProject jpaProject) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "dispatch: client command: {0}", command); //$NON-NLS-1$ this.execute(new ClientJobCommandWrapper(command, jpaProject), jobName, jpaProject.getProject()); } private void execute(Command command, String jobName, ISchedulingRule schedulingRule) { this.execute(new CommandJobCommandAdapter(command), jobName, schedulingRule); } private void execute(JobCommand command, String jobName, ISchedulingRule schedulingRule) { this.commandContext.execute(command, jobName, schedulingRule); } private void waitToExecute(Command command, String jobName, ISchedulingRule schedulingRule) throws InterruptedException { this.waitToExecute(new CommandJobCommandAdapter(command), jobName, schedulingRule); } private void waitToExecute(JobCommand command, String jobName, ISchedulingRule schedulingRule) throws InterruptedException { this.waitToExecute(command, jobName, schedulingRule, 0); } private boolean waitToExecute(Command command, String jobName, ISchedulingRule schedulingRule, long timeout) throws InterruptedException { return this.waitToExecute(new CommandJobCommandAdapter(command), jobName, schedulingRule, timeout); } /** * Check whether the specified scheduling rule * {@link ISchedulingRule#isConflicting(ISchedulingRule) conflicts} with the * {@link IJobManager#currentRule() rule held by the current thread}. * If the rules conflict, * execute the specified command directly (i.e. synchronously) to prevent a * deadlock. This should not cause a problem because if the current rule * conflicts with the specified rule, the current rule will also prevent any * other, conflicting, JPA project manager commands from executing. */ private boolean waitToExecute(JobCommand command, String jobName, ISchedulingRule schedulingRule, long timeout) throws InterruptedException { ISchedulingRule currentRule = this.getCurrentRule(); if ((currentRule != null) && schedulingRule.isConflicting(currentRule)) { JptJpaCorePlugin.instance().dumpStackTrace(TRACE_OPTION, "scheduling rule conflict: {0} vs. {1}", schedulingRule, currentRule); //$NON-NLS-1$ command.execute(new NullProgressMonitor()); return true; } return this.commandContext.waitToExecute(command, jobName, schedulingRule, timeout); } public void execute(Command command) throws InterruptedException { this.execute(command, null); } /** * NB: * This method is synchronized so only a single thread can * execute it at a time. */ public synchronized void execute(Command command, ExtendedCommandContext threadLocalModifySharedDocumentCommandContext) throws InterruptedException { this.setThreadLocalModifySharedDocumentCommandContext(threadLocalModifySharedDocumentCommandContext); this.executeCommandsSynchronously(); try { command.execute(); } finally { this.executeCommandsAsynchronously(); } // not really necessary - thread locals are GCed this.setThreadLocalModifySharedDocumentCommandContext(null); } /** * NB: * This method is called (via reflection) when the test plug-in is loaded. *

* See org.eclipse.jpt.jpa.core.tests.JptJpaCoreTestsPlugin#start(org.osgi.framework.BundleContext). * * @see #executeCommandsAsynchronously() */ private void executeCommandsSynchronously() throws InterruptedException { // de-activate Java events this.addJavaEventListenerFlag(FalseBooleanReference.instance()); // save the current context SimpleJobCommandContext oldContext = (SimpleJobCommandContext) this.commandContext; // install a new (not-yet-started) context SingleUseQueueingExtendedJobCommandContext newContext = this.buildSynchronousCommandContext(); this.commandContext = newContext; // wait for all the outstanding commands to finish oldContext.waitToExecute(NullCommand.instance()); // start up the new context (it will now execute any commands that // arrived while we were waiting on the outstanding commands) newContext.start(); } private SingleUseQueueingExtendedJobCommandContext buildSynchronousCommandContext() { return new SingleUseQueueingExtendedJobCommandContext(); } /** * @see #executeCommandsSynchronously() */ private void executeCommandsAsynchronously() { // no need to wait on a synchronous context... this.commandContext = this.buildAsynchronousCommandContext(); // re-activate Java events this.removeJavaEventListenerFlag(FalseBooleanReference.instance()); } private SimpleJobCommandContext buildAsynchronousCommandContext() { return new SimpleJobCommandContext(JptCommonCoreMessages.DALI_JOB_NAME); } // ********** job command wrapper ********** /* CU private */ class ClientJobCommandWrapper implements JobCommand { private final JobCommand jobCommand; private final JpaProject jpaProject; ClientJobCommandWrapper(JobCommand jobCommand, JpaProject jpaProject) { super(); if ((jobCommand == null) || (jpaProject == null)) { throw new NullPointerException(); } this.jobCommand = jobCommand; this.jpaProject = jpaProject; } public IStatus execute(IProgressMonitor monitor) { InternalJpaProjectManager.this.execute_(this.jobCommand, monitor, this.jpaProject); return Status.OK_STATUS; } @Override public String toString() { return ObjectTools.toString(this, this.jobCommand); } } /** * Execute the specified command only if the specified JPA project is still * around. (i.e. The JPA project may have been removed between the time the * client requested the JPA project manager to execute the command and the * time the command reached the front of the execution queue.) * Called by {@link ClientJobCommandWrapper#execute(IProgressMonitor)}. */ /* CU private */ void execute_(JobCommand command, IProgressMonitor monitor, JpaProject jpaProject) { if (this.jpaProjects.contains(jpaProject)) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "execute: client command: {0}", command); //$NON-NLS-1$ command.execute(monitor); } else { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "ignore: client command: {0}", command); //$NON-NLS-1$ } } // ********** resource change listener ********** /* CU private */ class ResourceChangeListener extends ResourceChangeAdapter { /** * PRE_UNINSTALL is the only facet event we use for * removing JPA projects. These are the cases where we listen for resource events. *

* Check for:

*/ @Override public void resourceChanged(IResourceChangeEvent event) { switch (event.getType()) { case IResourceChangeEvent.POST_CHANGE : this.processPostChangeEvent(event); break; // workspace or project events case IResourceChangeEvent.PRE_REFRESH : break; // ignore case IResourceChangeEvent.PRE_BUILD : break; // ignore case IResourceChangeEvent.POST_BUILD : this.processPostBuildEvent(event); break; // project-only events case IResourceChangeEvent.PRE_CLOSE : break; // ignore case IResourceChangeEvent.PRE_DELETE : break; // ignore default : break; } } private void processPostChangeEvent(IResourceChangeEvent event) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "Resource POST_CHANGE event: {0}", event.getResource()); //$NON-NLS-1$ this.processPostChangeDelta(event.getDelta()); } private void processPostChangeDelta(IResourceDelta delta) { IResource resource = delta.getResource(); switch (resource.getType()) { case IResource.ROOT : this.processPostChangeRootDelta(delta); break; case IResource.PROJECT : this.processPostChangeProjectDelta(delta); break; case IResource.FOLDER : this.processPostChangeFolderDelta((IFolder) resource, delta); break; case IResource.FILE : this.processPostChangeFileDelta((IFile) resource, delta); break; default : break; } } // ***** POST_CHANGE ROOT private void processPostChangeRootDelta(IResourceDelta delta) { this.processPostChangeDeltaChildren(delta); } // ***** POST_CHANGE PROJECT /** * Process the project first for the "opening" project case; so * the JPA project will not be built until the children are processed * and we see that the facet metadata file is added. * Otherwise the JPA project would be built and then we would * process the ADDED deltas for all the files in * the project we would've discovered during JPA project construction * (i.e. we would throw an exception because we would try to add a file * to the JPA project that it already contained). */ private void processPostChangeProjectDelta(IResourceDelta delta) { InternalJpaProjectManager.this.projectChanged(delta); this.processPostChangeDeltaChildren(delta); } // ***** POST_CHANGE FOLDER private void processPostChangeFolderDelta(IFolder folder, IResourceDelta delta) { if (folder.getName().equals(".settings")) { //$NON-NLS-1$ this.processPostChangeDeltaChildren(delta); } } // ***** POST_CHANGE FILE private void processPostChangeFileDelta(IFile file, IResourceDelta delta) { if (file.getName().equals(FACETED_PROJECT_FRAMEWORK_SETTINGS_FILE_NAME)) { this.checkForFacetFileChanges(file, delta); } } private void checkForFacetFileChanges(IFile file, IResourceDelta delta) { switch (delta.getKind()) { case IResourceDelta.ADDED : case IResourceDelta.REMOVED : case IResourceDelta.CHANGED : InternalJpaProjectManager.this.checkForJpaFacetTransition(file.getProject()); break; case IResourceDelta.ADDED_PHANTOM : break; // ignore case IResourceDelta.REMOVED_PHANTOM : break; // ignore default : break; } } private void processPostChangeDeltaChildren(IResourceDelta delta) { for (IResourceDelta child : delta.getAffectedChildren()) { this.processPostChangeDelta(child); // recurse } } /** * A post build event has occurred. * Check for whether the build was a "clean" build and completely * rebuild the appropriate JPA project. */ // ***** POST_BUILD private void processPostBuildEvent(IResourceChangeEvent event) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "Resource POST_BUILD event: {0}", event.getDelta().getResource()); //$NON-NLS-1$ if (event.getBuildKind() == IncrementalProjectBuilder.CLEAN_BUILD) { this.processPostCleanBuildDelta(event.getDelta()); } } private void processPostCleanBuildDelta(IResourceDelta delta) { IResource resource = delta.getResource(); switch (resource.getType()) { case IResource.ROOT : this.processPostCleanBuildDeltaChildren(delta); break; case IResource.PROJECT : this.processProjectPostCleanBuild((IProject) resource); break; case IResource.FOLDER : break; // ignore case IResource.FILE : break; // ignore default : break; } } private void processPostCleanBuildDeltaChildren(IResourceDelta delta) { for (IResourceDelta child : delta.getAffectedChildren()) { this.processPostCleanBuildDelta(child); // recurse } } private void processProjectPostCleanBuild(IProject project) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, "\tProject CLEAN event: {0}", project.getName()); //$NON-NLS-1$ InternalJpaProjectManager.this.projectPostCleanBuild(project); } } // ********** Java element change listener ********** /** * Forward the Java element change event back to the JPA project manager. */ /* CU private */ class JavaElementChangeListener implements IElementChangedListener { public void elementChanged(ElementChangedEvent event) { if (this.isActive()) { InternalJpaProjectManager.this.javaElementChanged(event); } } private boolean isActive() { return InternalJpaProjectManager.this.javaEventListenersAreActive(); } @Override public String toString() { return ObjectTools.toString(this, this.isActive() ? "active" : "inactive"); //$NON-NLS-1$ //$NON-NLS-2$ } } // ********** java events ********** /* CU private */ boolean javaEventListenersAreActive() { synchronized (this.javaEventListenerFlags) { return this.javaEventListenersAreActive_(); } } /** * All the flags must be true. */ private boolean javaEventListenersAreActive_() { for (BooleanReference flag : this.javaEventListenerFlags) { if (flag.isFalse()) { return false; } } return true; } public void addJavaEventListenerFlag(BooleanReference flag) { synchronized (this.javaEventListenerFlags) { this.addJavaEventListenerFlag_(flag); } } private void addJavaEventListenerFlag_(BooleanReference flag) { if ( ! this.javaEventListenerFlags.add(flag)) { throw new IllegalArgumentException("duplicate flag: " + flag); //$NON-NLS-1$ } } public void removeJavaEventListenerFlag(BooleanReference flag) { synchronized (this.javaEventListenerFlags) { this.removeJavaEventListenerFlag_(flag); } } private void removeJavaEventListenerFlag_(BooleanReference flag) { if ( ! this.javaEventListenerFlags.remove(flag)) { throw new IllegalArgumentException("missing flag: " + flag); //$NON-NLS-1$ } } // ********** tracing ********** /* CU private */ static final String TRACE_OPTION = JpaProjectManager.class.getSimpleName(); /** * This method should be invoked (reflectively) only from the tests. * See JpaProjectManagerTests.trace(String). */ @SuppressWarnings("unused") private static void trace(String message) { JptJpaCorePlugin.instance().trace(TRACE_OPTION, message); } }