/******************************************************************************* * Copyright (c) 2006 Oracle Corporation. * 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: * Cameron Bateman/Oracle - initial API and implementation * ********************************************************************************/ package org.eclipse.jst.jsf.designtime; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.concurrent.atomic.AtomicBoolean; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IFolder; import org.eclipse.core.resources.IProject; import org.eclipse.core.resources.IWorkspace; import org.eclipse.core.resources.IWorkspaceRunnable; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.runtime.CoreException; 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.QualifiedName; import org.eclipse.core.runtime.Status; import org.eclipse.jst.jsf.common.internal.resource.EventResult; import org.eclipse.jst.jsf.common.internal.resource.IResourceLifecycleListener; import org.eclipse.jst.jsf.common.internal.resource.LifecycleListener; import org.eclipse.jst.jsf.common.internal.resource.ResourceLifecycleEvent; import org.eclipse.jst.jsf.core.JSFVersion; import org.eclipse.jst.jsf.core.internal.JSFCorePlugin; import org.eclipse.jst.jsf.core.jsfappconfig.JSFAppConfigUtils; import org.eclipse.jst.jsf.designtime.context.AbstractDTExternalContextFactory; import org.eclipse.jst.jsf.designtime.context.DTFacesContext; import org.eclipse.jst.jsf.designtime.context.IExternalContextFactoryLocator; import org.eclipse.jst.jsf.designtime.el.AbstractDTMethodResolver; import org.eclipse.jst.jsf.designtime.el.AbstractDTPropertyResolver; import org.eclipse.jst.jsf.designtime.el.AbstractDTVariableResolver; import org.eclipse.jst.jsf.designtime.internal.BasicExtensionFactory.ExtensionData; import org.eclipse.jst.jsf.designtime.internal.view.AbstractDTViewHandler; import org.eclipse.jst.jsf.designtime.internal.view.IDTViewHandler; /** * Per-web-application manager that manages design time information for a * corresponding project. * * TODO: migrate to managed singleton * * @author cbateman * */ public final class DesignTimeApplicationManager { private static final String SETTINGS_DIR_NAME = ".settings"; //$NON-NLS-1$ private static final String ORG_ECLIPSE_JST_JSF_DESIGNTIME_APPMGR_PREFS = "org.eclipse.jst.jsf.designtime.appmgr.prefs"; //$NON-NLS-1$ private static final String PROPERTY_QUALIFIER = "org.eclipse.jst.jsf.designtime.internal"; //$NON-NLS-1$ private static final String SESSION_PROPERTY_NAME_PROJECT = "DesignTimeApplicationManager"; //$NON-NLS-1$ private static final QualifiedName SESSION_PROPERTY_KEY_PROJECT = new QualifiedName( PROPERTY_QUALIFIER, SESSION_PROPERTY_NAME_PROJECT); private static final String PERSIST_PROPERTY_NAME_EXTERNAL_CONTEXT_PROVIDER = "ExternalContextProvider"; //$NON-NLS-1$ private static final QualifiedName PERSIST_PROPERTY_KEY_EXTERNAL_CONTEXT_PROVIDER = new QualifiedName( PROPERTY_QUALIFIER, PERSIST_PROPERTY_NAME_EXTERNAL_CONTEXT_PROVIDER); private static final String PERSIST_PROPERTY_NAME_VARIABLE_RESOLVER_PROVIDER = "VariableResolverProvider"; //$NON-NLS-1$ private static final QualifiedName PERSIST_PROPERTY_KEY_VARIABLE_RESOLVER_PROVIDER = new QualifiedName( PROPERTY_QUALIFIER, PERSIST_PROPERTY_NAME_VARIABLE_RESOLVER_PROVIDER); private static final String PERSIST_PROPERTY_NAME_PROPERTY_RESOLVER_PROVIDER = "PropertyResolverProvider"; //$NON-NLS-1$ private static final QualifiedName PERSIST_PROPERTY_KEY_PROPERTY_RESOLVER_PROVIDER = new QualifiedName( PROPERTY_QUALIFIER, PERSIST_PROPERTY_NAME_PROPERTY_RESOLVER_PROVIDER); private static final String PERSIST_PROPERTY_NAME_METHOD_RESOLVER_PROVIDER = "MethodResolverProvider"; //$NON-NLS-1$ private static final QualifiedName PERSIST_PROPERTY_KEY_METHOD_RESOLVER_PROVIDER = new QualifiedName( PROPERTY_QUALIFIER, PERSIST_PROPERTY_NAME_METHOD_RESOLVER_PROVIDER); private static final String PERSIST_PROPERTY_NAME_VIEW_HANDLER = "ViewHandler"; //$NON-NLS-1$ private static final String DEFAULT_EXTERNAL_CONTEXT_ID = "org.eclipse.jst.jsf.core.externalcontext.default"; //$NON-NLS-1$ private static final String DEFAULT_VARIABLE_RESOLVER_ID = "org.eclipse.jst.jsf.core.variableresolver.default.decorative"; //$NON-NLS-1$ private static final String DEFAULT_PROPERTY_RESOLVER_ID = "org.eclipse.jst.jsf.core.propertyresolver.default.decorative"; //$NON-NLS-1$ private static final String DEFAULT_METHOD_RESOLVER_ID = "org.eclipse.jst.jsf.core.methodresolver.default"; //$NON-NLS-1$ private static final String PRE_20_DEFAULT_VIEW_HANDLER_ID = "org.eclipse.jst.jsf.designtime.view.jspviewhandler"; //$NON-NLS-1$ private static final String JSF_20_DEFAULT_VIEW_HANDLER_ID = "org.eclipse.jst.jsf.facelet.core.html.viewhandler"; //$NON-NLS-1$ private static final Object GET_INSTANCE_LOCK = new Object(); /** * @param project * @return the app manager associated with project */ public static DesignTimeApplicationManager getInstance( final IProject project) { if (!hasJSFDesignTime(project)) { return null; } try { synchronized (GET_INSTANCE_LOCK) { DesignTimeApplicationManager manager = (DesignTimeApplicationManager) project .getSessionProperty(SESSION_PROPERTY_KEY_PROJECT); if (manager != null && !project.equals(manager._project)) { if (!manager._isDisposed.get()) { manager.dispose(); } // bug 147729: pretend we starting with a new project (we kind of are) manager = null; } if (manager == null) { manager = new DesignTimeApplicationManager(project); project.setSessionProperty(SESSION_PROPERTY_KEY_PROJECT, manager); } return manager; } } catch (final CoreException ce) { Platform .getLog(JSFCorePlugin.getDefault().getBundle()) .log( new Status( IStatus.ERROR, JSFCorePlugin.getDefault().getBundle() .getSymbolicName(), 0, "Problem loading design time appmanager", new Exception(ce))); //$NON-NLS-1$ } return null; } // private void checkAndMaybeUpdateProject(final IProject project) // { // boolean needsPropertyStore = false; // // synchronized(this) // { // if (!project.equals(_project)) // { // _lifecycleListener.removeResource(_project); // _project = project; // _lifecycleListener.addResource(_project); // _properties.setProject(project); // needsPropertyStore = true; // } // } // // if (needsPropertyStore) // { // _properties.store(); // } // } private synchronized void dispose() { if (_isDisposed.compareAndSet(false, true)) { // dispose viewhandler removeViewHandler(); _lifecycleListener.dispose(); _facesContexts.clear(); } } /** * <p> * The criteria for a project having a JSF design time are: * </p> * * <ul> * <li>project is non-null.</li> * <li>project is accessible (project.isAccessible() == true)</li> * <li>project has a JSF facet (this implies that it's dependent facets are * also present).</li> * </ul> * * @param project * @return true if project can have a JSF DesignTimeApplicationManager * associated with it. getInstance(project) uses this determine if * should construct an instance for a project. */ public static boolean hasJSFDesignTime(final IProject project) { JSFVersion guessJSFVersion = JSFVersion.guessJSFVersion(project); return project != null && project.isAccessible() && (JSFAppConfigUtils.isValidJSFProject(project) || (guessJSFVersion != null && guessJSFVersion.compareTo(JSFVersion.V2_0) >= 0)); } // instance definition // _project must be writable in case the manager needs to be retargetted // after a rename/move etc. private final IProject _project; private final IExternalContextFactoryLocator _locator; private final LifecycleListener _lifecycleListener; private final AtomicBoolean _isDisposed; private final ViewHandlerManager _viewHandlerManager; private final Map<IFile, DTFacesContext> _facesContexts; private DesignTimeApplicationManager(final IProject project) { _project = project; _locator = new MyExternalContextFactoryLocator(); _isDisposed = new AtomicBoolean(); _lifecycleListener = new LifecycleListener(_project, ResourcesPlugin.getWorkspace()); _lifecycleListener.addListener(new IResourceLifecycleListener() { public EventResult acceptEvent(final ResourceLifecycleEvent event) { if (event.getAffectedResource() == _project) { if (event.getEventType() == ResourceLifecycleEvent.EventType.RESOURCE_INACCESSIBLE && event.getReasonType() == ResourceLifecycleEvent.ReasonType.RESOURCE_PROJECT_CLOSED) { dispose(); } } return EventResult.getDefaultEventResult(); } }); final PropertyFileManager properties = new PropertyFileManager(_project); _viewHandlerManager = new ViewHandlerManager(properties); _facesContexts = new HashMap<IFile, DTFacesContext>(); } /** * Method is thread-safe and may block the caller. * * @param file * must not be null * @return the faces context for the file or null if not found */ public DTFacesContext getFacesContext(final IFile file) { checkIsDisposed(); if (!hasDTFacesContext(file)) { return null; } synchronized (_facesContexts) { DTFacesContext context = _facesContexts.get(file); if (context == null) { context = new DTFacesContext(file, _locator); context.initialize(_lifecycleListener); _facesContexts.put(file, context); } return context; } } /** * Method is thread-safe and may block the caller. * * Only files for which a runtime request context will be generated have a * corresponding design time context. This is generally confined to view * definition files such as JSP's. * * General criteria for a file to have a design time faces context are: - * the file is non-null and isAccessible() - the file has designtime view * handler (getViewHandler(file) != null) and it supports the content type * of file. * * getFacesContext uses this to decide whether to generate a context for an * IFile. * * @param file * @return true if file has a design time faces context */ public boolean hasDTFacesContext(final IFile file) { checkIsDisposed(); final IDTViewHandler viewHandler = getViewHandler(); if (file != null && file.isAccessible() && viewHandler != null && viewHandler.supportsViewDefinition(file)) { return true; } return false; } /** * Method is thread-safe and may block the caller. * * @return the design time view handler for this webapp (project). */ public IDTViewHandler getViewHandler() { checkIsDisposed(); /* NOTE: it is critical that view handler calls _NEVER_ take the DesignTimeApplicationManager lock. let ViewHandlerManager manage it instead.*/ return _viewHandlerManager.getViewHandler(_project, _lifecycleListener); } /** * Remove and dispose of any currently registered view handler */ private void removeViewHandler() { /* NOTE: it is critical that view handler calls _NEVER_ take the DesignTimeApplicationManager lock. let ViewHandlerManager manage it instead.*/ _viewHandlerManager.removeViewHandler(_project); } /** * Method is thread-safe and may block the caller. * * Sets the persistent id on this project that will be used to load the view * handler. * * @param viewHandlerId */ public void setViewHandlerId(final String viewHandlerId) { checkIsDisposed(); /* NOTE: it is critical that view handler calls _NEVER_ take the DesignTimeApplicationManager lock. let ViewHandlerManager manage it instead.*/ _viewHandlerManager.setViewHandlerId(_project, viewHandlerId); } /** * Method is thread-safe and may block the caller. * * @param resolverPluginId * @throws CoreException */ public synchronized void setExternalContextProvider( final String resolverPluginId) throws CoreException { checkIsDisposed(); _project.setPersistentProperty( PERSIST_PROPERTY_KEY_EXTERNAL_CONTEXT_PROVIDER, resolverPluginId); } /** * Method is thread-safe and may block the caller. * * @return the id of the active design time external context provider */ public synchronized String getExternalContextProvider() { checkIsDisposed(); return getResolverId(PERSIST_PROPERTY_KEY_EXTERNAL_CONTEXT_PROVIDER, DEFAULT_EXTERNAL_CONTEXT_ID); } /** * Method is thread-safe and may block the caller. * * @return the designtime variable resolver for this application */ public synchronized AbstractDTVariableResolver getVariableResolver() { checkIsDisposed(); ExtensionData<AbstractDTVariableResolver> extData = null; final String id = getResolverId_OLD(PERSIST_PROPERTY_KEY_VARIABLE_RESOLVER_PROVIDER); if (id != null) { extData = JSFCorePlugin.getVariableResolvers(id); } if (extData == null) { extData = JSFCorePlugin .getVariableResolvers(DEFAULT_VARIABLE_RESOLVER_ID); } return extData.getInstance(_project); } /** * Method is thread-safe and may block the caller. * * Sets the plugin used to determine the designtime variable resolver. To * reset to the default, pass null. * * @param resolverPluginId * @throws CoreException -- * if the setting the new value fails */ public synchronized void setVariableResolverProvider( final String resolverPluginId) throws CoreException { checkIsDisposed(); _project.setPersistentProperty( PERSIST_PROPERTY_KEY_VARIABLE_RESOLVER_PROVIDER, resolverPluginId); } /** * Method is thread-safe and may block the caller. * * @return the id of the active design time variable resolver */ public synchronized String getVariableResolverProvider() { checkIsDisposed(); return getResolverId(PERSIST_PROPERTY_KEY_VARIABLE_RESOLVER_PROVIDER, DEFAULT_VARIABLE_RESOLVER_ID); } /** * Method is thread-safe and may block the caller. * * @return the default property resolver that will be used if no other is * provided. The default property resolver is intended to match the * similar resolver used by the runtime. */ public synchronized AbstractDTPropertyResolver getDefaultPropertyResolver() { checkIsDisposed(); return JSFCorePlugin.getPropertyResolver(DEFAULT_PROPERTY_RESOLVER_ID) .getInstance(_project); } /** * Method is thread-safe and may block the caller. * * @return the designtime property resolver for this application */ public synchronized AbstractDTPropertyResolver getPropertyResolver() { checkIsDisposed(); ExtensionData<AbstractDTPropertyResolver> extData = null; final String id = getResolverId_OLD(PERSIST_PROPERTY_KEY_PROPERTY_RESOLVER_PROVIDER); if (id != null) { extData = JSFCorePlugin.getPropertyResolver(id); } if (extData == null) { extData = JSFCorePlugin .getPropertyResolver(DEFAULT_PROPERTY_RESOLVER_ID); } return extData.getInstance(_project); } /** * Method is thread-safe and may block the caller. * * @param resolverPluginId * @throws CoreException -- * if setting the provider fails */ public synchronized void setPropertyResolverProvider( final String resolverPluginId) throws CoreException { checkIsDisposed(); _project.setPersistentProperty( PERSIST_PROPERTY_KEY_PROPERTY_RESOLVER_PROVIDER, resolverPluginId); } /** * Method is thread-safe and may block the caller. * * @return the id of the active design time variable resolver */ public synchronized String getPropertyResolverProvider() { checkIsDisposed(); return getResolverId(PERSIST_PROPERTY_KEY_PROPERTY_RESOLVER_PROVIDER, DEFAULT_PROPERTY_RESOLVER_ID); } /** * Method is thread-safe and may block the caller. * * @return the designtime method resolver for this application */ public synchronized AbstractDTMethodResolver getMethodResolver() { checkIsDisposed(); ExtensionData<AbstractDTMethodResolver> extData = null; final String id = getResolverId_OLD(PERSIST_PROPERTY_KEY_METHOD_RESOLVER_PROVIDER); if (id != null) { extData = JSFCorePlugin.getMethodResolvers(id); } if (extData == null) { extData = JSFCorePlugin .getMethodResolvers(DEFAULT_METHOD_RESOLVER_ID); } return extData.getInstance(_project); } /** * Method is thread-safe and may block the caller. * * @param resolverPluginId * @throws CoreException -- * if setting the plugin fails */ public synchronized void setMethodResolverProvider( final String resolverPluginId) throws CoreException { checkIsDisposed(); _project .setPersistentProperty( PERSIST_PROPERTY_KEY_METHOD_RESOLVER_PROVIDER, resolverPluginId); } /** * Method is thread-safe and may block the caller. * * @return the id of the active design time variable resolver */ public synchronized String getMethodResolverProvider() { checkIsDisposed(); return getResolverId(PERSIST_PROPERTY_KEY_METHOD_RESOLVER_PROVIDER, DEFAULT_METHOD_RESOLVER_ID); } private String getResolverId_OLD(final QualifiedName pluginKey) { String pluginId = null; try { pluginId = _project.getPersistentProperty(pluginKey); } catch (final CoreException ce) { JSFCorePlugin.getDefault().getLog().log( new Status(IStatus.ERROR, JSFCorePlugin.PLUGIN_ID, 0, "Error getting plugin property", //$NON-NLS-1$ ce)); pluginId = null; // fall-through and use the default } return pluginId; } private String getResolverId(final QualifiedName key, final String defaultValue) { String id = defaultValue; try { final String userId = _project.getPersistentProperty(key); if (userId != null) { id = userId; } } catch (final CoreException ce) { // do nothing; fall through and return default } return id; } private class MyExternalContextFactoryLocator implements IExternalContextFactoryLocator { public AbstractDTExternalContextFactory getFactory() { ExtensionData<AbstractDTExternalContextFactory> extData = null; final String id = getResolverId_OLD(PERSIST_PROPERTY_KEY_EXTERNAL_CONTEXT_PROVIDER); if (id != null) { extData = JSFCorePlugin.getExternalContextProviders(id); } if (extData == null) { extData = JSFCorePlugin .getExternalContextProviders(DEFAULT_EXTERNAL_CONTEXT_ID); } return extData.getInstance(_project); } } private void checkIsDisposed() { // TODO: need to add isDisposed to this and throw an exception // for now, we just log what's happening to aid debugging if (_isDisposed.get()) { JSFCorePlugin .log( "A call to a disposed DesignTimeApplicationManager was attempted", //$NON-NLS-1$ new Throwable( "This exception is only to record a stack trace")); //$NON-NLS-1$ } } private static class ViewHandlerManager { private final PropertyFileManager _propertyFileManager; ViewHandlerManager(final PropertyFileManager properties) { _propertyFileManager = properties; } public synchronized void removeViewHandler(final IProject project) { final String viewHandlerId = getHandlerId(project); if (viewHandlerId != null) { final ExtensionData<AbstractDTViewHandler> extData = JSFCorePlugin.getViewHandlers(viewHandlerId); if (extData != null) { final AbstractDTViewHandler handler = extData.removeInstance(project); if (handler != null) { handler.dispose(); } } } } protected String getHandlerId(final IProject project) { JSFVersion projectVersion = JSFVersion.guessJSFVersion(project); if (projectVersion != null) { String defaultHandler = PRE_20_DEFAULT_VIEW_HANDLER_ID; // starting with JSF 2.0 a new view handler that first // processes as Facelet and then delegates to JSP is // used by default if (projectVersion.compareTo(JSFVersion.V2_0) >= 0) { defaultHandler = JSF_20_DEFAULT_VIEW_HANDLER_ID; } final String viewHandlerId = _propertyFileManager .getProperty(PERSIST_PROPERTY_NAME_VIEW_HANDLER, defaultHandler); return viewHandlerId; } return null; } public synchronized IDTViewHandler getViewHandler( final IProject project, final LifecycleListener listener) { final String viewHandlerId = getHandlerId(project); if (viewHandlerId != null) { ExtensionData<AbstractDTViewHandler> viewHandlers = JSFCorePlugin .getViewHandlers(viewHandlerId); if (viewHandlers == null) { viewHandlers = JSFCorePlugin .getViewHandlers(PRE_20_DEFAULT_VIEW_HANDLER_ID); } AbstractDTViewHandler viewHandler = viewHandlers .getInstance(project); if (JSFVersion.guessAtLeast(JSFVersion.V2_0, project)) { final AbstractDTViewHandler overrideHandler = JSFCorePlugin.getViewOverrideHandler(viewHandler); if (overrideHandler != null) { overrideHandler.setParent(viewHandler); viewHandler = overrideHandler; } } viewHandler.setLifecycleListener(listener); return viewHandler; } return null; } public void setViewHandlerId(final IProject project, final String viewHandlerId) { // remove any previous handler before the id is lost removeViewHandler(project); _propertyFileManager.setProperty( PERSIST_PROPERTY_NAME_VIEW_HANDLER, viewHandlerId); } } private static class PropertyFileManager { private final Properties _properties; private IProject _project; PropertyFileManager(final IProject project) { _project = project; _properties = new Properties(); load(_project, _properties); } private synchronized IProject getProject() { return _project; } public void setProperty(final String key, final String value) { _properties.setProperty(key, value); store(); } public String getProperty(final String key, final String defaultValue) { return _properties.getProperty(key, defaultValue); } public void store() { final IWorkspaceRunnable storeJob = new IWorkspaceRunnable() { public void run(IProgressMonitor monitor) throws CoreException { IFile propFile; try { propFile = getPropsFile(getProject()); if (propFile != null) { final ByteArrayOutputStream outstream = new ByteArrayOutputStream(); // properties will thread-safe save to the array // stream // no further locking is needed since we have // already have a lock // on the workspace sub-tree for the settings file // if this job // is running. _properties.store(outstream, null); propFile.setContents(new ByteArrayInputStream( outstream.toByteArray()), true, true, null); } } catch (final CoreException e) { JSFCorePlugin.log(e, "Problem storing properties"); //$NON-NLS-1$ } catch (final IOException e) { JSFCorePlugin.log(e, "Problem storing properties"); //$NON-NLS-1$ } } }; final IWorkspace workspace = ResourcesPlugin.getWorkspace(); try { // need to lock the project tree, since may need to create // .settings directory workspace.run(storeJob, getProject(), IWorkspace.AVOID_UPDATE, null); } catch (CoreException ce) { JSFCorePlugin.log(ce, "Problem storing properties"); //$NON-NLS-1$ } } private static void load(final IProject project, final Properties properties) { try { final IFile propFile = getPropsFileHandle(project); if (propFile != null && propFile.isAccessible()) { InputStream inStream = null; try { inStream = propFile.getContents(); properties.load(inStream); } finally { if (inStream != null) { inStream.close(); } } } } catch (final CoreException ce) { JSFCorePlugin.log(ce, "Problem loading properties"); //$NON-NLS-1$ } catch (final IOException ce) { JSFCorePlugin.log(ce, "Problem loading properties"); //$NON-NLS-1$ } } private static IFile getPropsFile(final IProject project) throws CoreException { final IFolder folder = project.getFolder(new Path(SETTINGS_DIR_NAME)); if (!folder.exists()) { folder.create(false, true, null); } final IFile file = folder.getFile(new Path(ORG_ECLIPSE_JST_JSF_DESIGNTIME_APPMGR_PREFS)); if (!file.exists()) { file.create(new ByteArrayInputStream(new byte[0]), false, null); } return file; } /** * @param project * @return the file handle for the properties file. Doesn't create the * resource if it doesn't exist */ private static IFile getPropsFileHandle(final IProject project) { return project.getFile(new Path(SETTINGS_DIR_NAME).append(ORG_ECLIPSE_JST_JSF_DESIGNTIME_APPMGR_PREFS)); } } }