blob: 94a2b59184ed16988e93aaed46fc0b45331b8c0f [file] [log] [blame]
/***********************************************************************
* Copyright (c) 2008 by SAP AG, Walldorf.
* 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:
* SAP AG - initial API and implementation
***********************************************************************/
package org.eclipse.jst.jee.model.internal.common;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
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.ISafeRunnable;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.SafeRunner;
import org.eclipse.core.runtime.Status;
import org.eclipse.core.runtime.jobs.Job;
import org.eclipse.jdt.core.IJavaProject;
import org.eclipse.jdt.core.JavaCore;
import org.eclipse.jst.j2ee.model.IModelProvider;
import org.eclipse.jst.j2ee.model.IModelProviderEvent;
import org.eclipse.jst.j2ee.model.IModelProviderListener;
import org.eclipse.jst.javaee.core.JavaEEObject;
import org.eclipse.jst.javaee.core.SecurityRole;
import org.eclipse.jst.javaee.core.SecurityRoleRef;
import org.eclipse.jst.javaee.ejb.SessionBean;
import org.eclipse.wst.common.project.facet.core.IFacetedProject;
/**
* @author Kiril Mitov k.mitov@sap.com
*
*/
public abstract class AbstractAnnotationModelProvider<T> implements IResourceChangeListener, IModelProvider {
/**
* Find the security role with the given name in the given assembly
* descriptor.
*
* @param assembly
* @param name
* @return <code>null</code> if a security role with this name can not be
* found
*/
private static SecurityRole findRole(Collection<SecurityRole> securityRoles, String name) {
for (SecurityRole role : securityRoles) {
if (role.getRoleName().equals(name))
return role;
}
return null;
}
protected T modelObject;
private Collection<IModelProviderListener> listeners;
private Lock listenersLock = new ReentrantLock();
protected IFacetedProject facetedProject;
private ManyToOneRelation<SecurityRoleRef, SecurityRole> rolesToRolesRef = new ManyToOneRelation<SecurityRoleRef, SecurityRole>();
/**
* Constructs a new AnnotationReader for this faceted project. An illegal
* argument if a project with value <code>null</code> is passed. No
* loading is done in this constructor. Loading the model is made on demand
* when calling {@link #getModelObject()}.
*
* @param project
* the ejb project. Can not be <code>null</code>
*/
public AbstractAnnotationModelProvider(IFacetedProject project) {
if (project == null)
throw new IllegalArgumentException("The project argument can not be null");
this.facetedProject = project;
}
public T getConcreteModel() {
if (modelObject == null) {
preLoad();
try {
loadModel();
/*
* Adding the resource change listener after loading the model.
* No resource change event are acceptable while loading the
* model.
*/
postLoad();
} catch (CoreException e) {
log(e.getStatus());
return null;
}
}
return modelObject;
}
public Object getModelObject() {
return getConcreteModel();
}
public Object getModelObject(IPath modelPath) {
return getConcreteModel();
}
protected abstract void loadModel() throws CoreException;
protected void preLoad() {
}
protected void postLoad() {
ResourcesPlugin.getWorkspace().addResourceChangeListener(this,
IResourceChangeEvent.POST_BUILD | IResourceChangeEvent.PRE_CLOSE | IResourceChangeEvent.PRE_DELETE);
}
/**
* Notifies the currently registered listeners with this model event. If the
* {@link IModelProviderEvent#getChangedResources()} is empty or
* <code>null</code> the method returns immediately.
*
* @param event
* the event that should be send to the listeners
*/
protected void notifyListeners(final IModelProviderEvent event) {
notifyListeners(listeners, event);
}
/**
* Clears the list of listeners. No notifications can occur while clearing
* the listeners.
*/
protected void clearListeners() {
if (listeners == null)
return;
try {
listenersLock.lock();
listeners.clear();
listeners = null;
} finally {
listenersLock.unlock();
}
}
private void notifyListeners(final Collection<IModelProviderListener> aListeners, final IModelProviderEvent event) {
if (listeners == null)
return;
listenersLock.lock();
try {
if (event.getChangedResources() == null || event.getChangedResources().isEmpty())
return;
for (final IModelProviderListener listener : aListeners) {
SafeRunner.run(new ISafeRunnable() {
public void handleException(Throwable exception) {
}
public void run() throws Exception {
listener.modelsChanged(event);
}
});
}
} finally {
listenersLock.unlock();
}
}
/**
* @return the currently registered listeners.
*/
protected Collection<IModelProviderListener> getListeners() {
if (listeners == null) {
listeners = new HashSet<IModelProviderListener>();
}
return listeners;
}
/**
* Adds a listener to this instance. No listeners can be added during
* notifying the current listeners.
*
* @param listener
*/
public void addListener(IModelProviderListener listener) {
listenersLock.lock();
try {
getModelObject();
getListeners().add(listener);
} finally {
listenersLock.unlock();
}
}
/**
* Removes the listener from this instance. Has no effect if an identical
* listener is not registered.
*
* @param listener
* the listener to be removed.
*/
public void removeListener(IModelProviderListener listener) {
listenersLock.lock();
try {
getListeners().remove(listener);
} finally {
listenersLock.unlock();
}
}
/**
* @param project
* @return true if the given project contains resources that are relative to
* the model. This method returns <code>true</code> for the
* ejbProject on which this instance is working a <code>true</code>
* for its client project.
*/
protected boolean isProjectRelative(IProject project) {
if (project == null || facetedProject == null)
return false;
else if (project.equals(facetedProject.getProject()))
return true;
return false;
}
/**
* Dispose the current instance. The actual dispose may occur in another
* thread. Use {@link #addListener(IModelProviderListener)} to register a
* listener that will be notified when the instance is disposed. After all
* the listeners are notified the list of listeners is cleared.
*/
public abstract void dispose();
/**
* An internal refresh of the model. Depending on the type of the event and
* the amount of work that must be done the refresh may occur in the same or
* in another thread. Use {@link #addListener(IModelProviderListener)} to
* register a listener that will be notified when updating the model is
* finished.
*
* Changing a resource can also mean disposing the instance it the resource
* change event describes that a project is deleted or closed.
*
* @see org.eclipse.core.resources.IResourceChangeListener#resourceChanged(org.eclipse.core.resources.IResourceChangeEvent)
*/
public void resourceChanged(final IResourceChangeEvent event) {
// if (ejbJar == null)
// return;
final IModelProviderEvent modelEvent = new MyModelProviderEvent(0, null, facetedProject.getProject());
switch (event.getType()) {
case IResourceChangeEvent.PRE_DELETE:
case IResourceChangeEvent.PRE_CLOSE:
IProject project = (IProject) event.getResource();
if (!isProjectRelative(project))
return;
dispose();
break;
case IResourceChangeEvent.POST_BUILD:
internalRefresh(modelEvent, event.getDelta());
}
}
/**
* Internal refresh of the model maintained by this instance. Calling this
* method usually means that there is a descent amount of work to be
* processed. The method is processing the added/remove/change java files
* described by "projectDelta"
*
* Specific information for which objects were changed will be added in the
* modelEvent
*
* Refreshing the model may occurs in another thread. Use
* {@link #addListener(IModelProviderListener)} to register a listener. The
* listener will be notified when refreshing is finished.
*
* @param modelEvent
* @param projectDelta
*/
protected void internalRefresh(final IModelProviderEvent modelEvent, final IResourceDelta projectDelta) {
final CollectiveResourceDeltaVisitor visitor = collectDeltaFiles(projectDelta);
Job job = new Job("Refresh annotation model") {
@Override
protected IStatus run(IProgressMonitor monitor) {
try {
for (IFile file : visitor.getRemovedFiles()) {
processRemovedFile(modelEvent, file);
}
for (IFile file : visitor.getAddedFiles()) {
processAddedFile(modelEvent, file);
}
for (IFile file : visitor.getChangedFiles()) {
processChangedFile(modelEvent, file);
}
visitor.clear();
} catch (CoreException e) {
return e.getStatus();
} finally {
/*
* I do not provide a backup version of the model from which
* a revert is possible so I am notifying the listeners even
* if an error has occurred. Since the model might be
* changed in one thread an the notification will occur in
* another thread there might be listener depending on this
* notifications to unblock a blocked thread.
*
* It is possible in case of an exception to rebuild the
* model from scratch or to send a notification that the
* model is not valid. This is is still open question.
*/
notifyListeners(modelEvent);
}
return Status.OK_STATUS;
}
};
job.schedule();
}
protected CollectiveResourceDeltaVisitor collectDeltaFiles(IResourceDelta projectDelta) {
CollectiveResourceDeltaVisitor visitor = new CollectiveResourceDeltaVisitor(facetedProject.getProject());
try {
projectDelta.accept(visitor);
} catch (CoreException e1) {
log(e1.getStatus());
e1.printStackTrace();
}
return visitor;
}
/**
* Process a file as "removed". The method is allowed not to make checks
* whether the file was added/removed/change. It is processing the file as
* "removed".
*
* <p>
* This method changes the model. The method is called by
* {@link #internalRefresh(IModelProviderEvent, IResourceDelta)}.
* Implementations should assure changes to the model are synchronized
* between each other.
* </p>
*
* If no model object depends on the given file "modelEvent" is not changed.
*
* @see #internalRefresh(IModelProviderEvent, IResourceDelta)
* @see #processAddedFile(IModelProviderEvent, IFile)
* @param modelEvent
* @param file
* the file to be removed.
* @throws CoreException
* if there was an error during parsing the file
*/
protected abstract void processRemovedFile(IModelProviderEvent modelEvent, IFile file) throws CoreException;
/**
* Process a file as "added". The method is allowed not to make checks
* whether the file was added/removed/change. It is processing the file as
* "added". It is the responsibility of the caller to make sure the
* processing of the file as added will not leave the model in a wrong
* state.
*
* modelEvent is changed to contain information about the added modelObject.
*
* <p>
* This method changes the model. The method is called by
* {@link #internalRefresh(IModelProviderEvent, IResourceDelta)}.
* Implementations should assure changes to the model are synchronized
* between each other.
* </p>
*
* @see #processChangedFile(IModelProviderEvent, IFile)
* @see #processRemovedFile(IModelProviderEvent, IFile)
* @param modelEvent
* @param file
* the file that was added
* @throws CoreException
*/
protected abstract void processAddedFile(IModelProviderEvent modelEvent, IFile file) throws CoreException;
/**
* Process a file as "changed". The method is allowed not to make checks
* whether the file was added/removed/change. It is processing the file as
* "changed". It is the responsibility of the caller to make sure the
* processing of the file as "changed" will not leave the model in a wrong
* state.
*
* <p>
* This method changes the model. The method is called by
* {@link #internalRefresh(IModelProviderEvent, IResourceDelta)}.
* Implementations should assure changes to the model are synchronized
* between each other.
* </p>
*
* @see #processAddedFile(IModelProviderEvent, IFile)
* @see #processRemovedFile(IModelProviderEvent, IFile)
* @param modelEvent
* @param file
* the file that was changed
* @throws CoreException
*/
protected abstract void processChangedFile(IModelProviderEvent modelEvent, IFile file) throws CoreException;
protected void log(IStatus status) {
}
protected MyModelProviderEvent createModelProviderEvent() {
return new MyModelProviderEvent(0, null, facetedProject.getProject());
}
/**
* @author Kiril Mitov k.mitov@sap.com
*
*/
protected class CollectiveResourceDeltaVisitor implements IResourceDeltaVisitor {
private Collection<IFile> addedFiles;
private Collection<IFile> removedFiles;
private Collection<IFile> changedFiles;
private IJavaProject ejbProjectJavaView;
public CollectiveResourceDeltaVisitor(IProject ejbProject) {
addedFiles = new ArrayList<IFile>();
removedFiles = new ArrayList<IFile>();
changedFiles = new ArrayList<IFile>();
this.ejbProjectJavaView = JavaCore.create(ejbProject);
}
public Collection<IFile> getAddedFiles() {
return addedFiles;
}
public Collection<IFile> getRemovedFiles() {
return removedFiles;
}
public Collection<IFile> getChangedFiles() {
return changedFiles;
}
public void clear() {
addedFiles.clear();
removedFiles.clear();
changedFiles.clear();
}
private boolean isOnClasspath(IFile file) {
return ejbProjectJavaView.isOnClasspath(file);
}
public boolean visit(IResourceDelta delta) throws CoreException {
if (delta.getResource().getType() == IResource.PROJECT) {
return isProjectRelative((IProject) delta.getResource());
}
if (delta.getResource().getType() == IResource.FILE) {
IFile file = (IFile) delta.getResource();
if (!isOnClasspath(file))
return false;
if ("java".equals(file.getFileExtension())) {
switch (delta.getKind()) {
case IResourceDelta.ADDED:
addedFiles.add((IFile) delta.getResource());
break;
case IResourceDelta.CHANGED:
changedFiles.add((IFile) delta.getResource());
break;
case IResourceDelta.REMOVED:
removedFiles.add((IFile) delta.getResource());
break;
}
}
return false;
}
return true;
}
}
// ---------------SECURITY ROLES ---------------------------//
protected abstract Collection<SecurityRole> getSecurityRoles();
protected abstract Collection<SecurityRoleRef> getSecurityRoleRefs(JavaEEObject target);
/**
* Deletes the connection maintained by the given bean and the security
* roles defined in the bean. If this is the only bean in which the role is
* defined, the role will also be deleted. Calling this method makes sense
* only if the bean and the security role and the bean were connected with
* {@link #connectWithRole(SecurityRole, SessionBean)}
*
* <p>
* If the bean is not of type org.eclipse.jst.javaee.ejb.SessionBean the
* method returns immediately.
* </p>
*
* @see #connectWithRole(SecurityRole, SessionBean)
* @see #rolesToRolesRef
* @param bean
*/
protected void disconnectFromRoles(JavaEEObject target) {
Collection<SecurityRole> roles = getSecurityRoles();
if (roles == null)
return;
Collection<SecurityRoleRef> refs = getSecurityRoleRefs(target);
if (refs == null)
return;
for (SecurityRoleRef ref : refs) {
SecurityRole role = rolesToRolesRef.getTarget(ref);
rolesToRolesRef.disconnectSource(ref);
if (!rolesToRolesRef.containsTarget(role)) {
getSecurityRoles().remove(role);
}
}
}
/**
* A security role was found in the given file. Add this security role to
* the assembly descriptor. If the ejbJar does not have an assembly
* descriptor a new one is created.
*
* @see #connectRoleWithBean(SecurityRole, SessionBean)s
* @param file
* @param securityRole
*/
protected void securityRoleFound(JavaEEObject object, SecurityRole securityRole) {
connectWithRole(securityRole, object);
}
/**
* A security role can be defined in more the one bean. A bean can define
* more then one security role. This means we have a many-to-many relation
* between sessionBeans and securityRoles.
*
* <p>
* Luckily a sessionBean contains a list of securityRoleRefs. This method
* creates a connection between the securityRole contained in the assembly
* descriptor and the security role ref contained in the bean.
*
* If a security role is define only in one bean, deleting the bean means
* deleting the security role. But if the security role is defined in two
* beans only deleting both beans will result in deleting the security role.
* </p>
*
* @see #disconnectFromRoles(JavaEEObject)
* @see #rolesToRolesRef
* @param securityRole
* @param target
*/
private void connectWithRole(SecurityRole securityRole, JavaEEObject target) {
Collection<SecurityRole> roles = getSecurityRoles();
if (roles == null)
return;
Collection<SecurityRoleRef> refs = getSecurityRoleRefs(target);
if (refs == null)
return;
/*
* If there is a security role with this name use the existing security
* role.
*/
SecurityRole role = findRole(roles, securityRole.getRoleName());
if (role == null) {
roles.add(securityRole);
role = securityRole;
}
for (SecurityRoleRef ref : refs) {
if (ref.getRoleName().equals(role.getRoleName()))
rolesToRolesRef.connect(ref, role);
}
}
}