/******************************************************************************* * 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.internal.jsp; import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.concurrent.locks.ReentrantLock; import org.eclipse.core.resources.IFile; import org.eclipse.core.resources.IWorkspaceRunnable; import org.eclipse.core.resources.ResourcesPlugin; import org.eclipse.core.resources.WorkspaceJob; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IProgressMonitor; import org.eclipse.core.runtime.IStatus; import org.eclipse.core.runtime.OperationCanceledException; import org.eclipse.core.runtime.Platform; import org.eclipse.core.runtime.Status; import org.eclipse.core.runtime.jobs.IJobChangeEvent; import org.eclipse.core.runtime.jobs.ILock; import org.eclipse.core.runtime.jobs.Job; import org.eclipse.core.runtime.jobs.JobChangeAdapter; import org.eclipse.jst.jsf.common.JSFCommonPlugin; 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.common.internal.resource.ResourceLifecycleEvent.EventType; import org.eclipse.jst.jsf.common.internal.resource.ResourceLifecycleEvent.ReasonType; import org.eclipse.jst.jsf.common.metadata.Entity; import org.eclipse.jst.jsf.common.metadata.Trait; import org.eclipse.jst.jsf.common.metadata.internal.IMetaDataDomainContext; import org.eclipse.jst.jsf.common.metadata.internal.TraitValueHelper; import org.eclipse.jst.jsf.common.metadata.query.internal.MetaDataQueryContextFactory; import org.eclipse.jst.jsf.common.metadata.query.internal.MetaDataQueryFactory; import org.eclipse.jst.jsf.common.metadata.query.internal.taglib.ITaglibDomainMetaDataQuery; import org.eclipse.jst.jsf.context.resolver.structureddocument.IStructuredDocumentContextResolverFactory; import org.eclipse.jst.jsf.context.resolver.structureddocument.ITaglibContextResolver; import org.eclipse.jst.jsf.context.structureddocument.IStructuredDocumentContext; import org.eclipse.jst.jsf.context.structureddocument.IStructuredDocumentContextFactory; import org.eclipse.jst.jsf.context.symbol.IComponentSymbol; import org.eclipse.jst.jsf.context.symbol.ISymbol; import org.eclipse.jst.jsf.context.symbol.SymbolFactory; import org.eclipse.jst.jsf.context.symbol.source.AbstractContextSymbolFactory; import org.eclipse.jst.jsf.context.symbol.source.ISymbolConstants; import org.eclipse.jst.jsf.core.internal.JSFCorePlugin; import org.eclipse.jst.jsf.designtime.DesignTimeApplicationManager; import org.eclipse.jst.jsf.designtime.context.DTFacesContext; import org.eclipse.osgi.util.NLS; import org.eclipse.wst.sse.core.StructuredModelManager; import org.eclipse.wst.sse.core.internal.provisional.IModelManager; import org.eclipse.wst.sse.core.internal.provisional.IStructuredModel; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMDocument; import org.eclipse.wst.xml.core.internal.provisional.document.IDOMModel; import org.w3c.dom.Node; import org.w3c.dom.NodeList; /** * Processes a JSP model to determine information of interest about it such * as what tags are currently in use. Listens to the model and updates it's * information when the model changes. * * @author cbateman * */ public class JSPModelProcessor { /** * Pass to force refresh arguments */ public final static boolean FORCE_REFRESH = true; /** * Pass to runAfter argument of refresh to indicate there is nothing * to run when the refresh job completes. */ public final static Runnable NO_RUN_AFTER = null; /** * Pass to runAfter argument of refresh to indicate that the caller * should be blocked until the job completes. */ public final static Runnable RUN_ON_CURRENT_THREAD = new Runnable() { public void run() { // do nothing } }; /** * An init-time setting that is used to stop the model processor from * automatically refreshing when the file it is tracking changes. The can * only be set once at init is permanent for the static life timpe of * JSPModelProcessor. * * Note that it does not turn off listening for file delete events because * the singleton management still needs to know if it can dispose of an * instance. */ private final static boolean DISABLE_WKSPACE_CHANGE_REFRESH = System .getProperty("org.eclipse.jst.jsf.jspmodelprocessor.disable.wkspace.change.refresh") != null; //$NON-NLS-1$ private final static Map<IFile, JSPModelProcessor> RESOURCE_MAP = new HashMap<IFile, JSPModelProcessor>(); private final static java.util.concurrent.locks.Lock CRITICAL_SECTION = new ReentrantLock(); private static LifecycleListener LIFECYCLE_LISTENER; /** * @param file The file to get the model processor for * @return the processor for a particular model, creating it if it does not * already exist * @throws CoreException if an attempt to get the model associated with file * fails due to reasons other than I/O problems */ public static JSPModelProcessor get(final IFile file) throws CoreException { CRITICAL_SECTION.lock(); try { if (!file.isAccessible()) { throw new CoreException(new Status(IStatus.ERROR, JSFCorePlugin.PLUGIN_ID, "File must be accessible")); //$NON-NLS-1$ } JSPModelProcessor processor = RESOURCE_MAP.get(file); if (processor == null) { if (LIFECYCLE_LISTENER == null) { LIFECYCLE_LISTENER = new LifecycleListener(file, ResourcesPlugin.getWorkspace()); } else { LIFECYCLE_LISTENER.addResource(file); } processor = new JSPModelProcessor(file,LIFECYCLE_LISTENER); RESOURCE_MAP.put(file, processor); } return processor; } finally { CRITICAL_SECTION.unlock(); } } /** * Disposes of the JSPModelProcessor associated with model * @param file the model processor to be disposed */ private static void dispose(final IFile file) { CRITICAL_SECTION.lock(); try { final JSPModelProcessor processor = RESOURCE_MAP.get(file); if (processor != null) { RESOURCE_MAP.remove(file); if (!processor.isDisposed()) { processor.dispose(); LIFECYCLE_LISTENER.removeResource(file); } } if (RESOURCE_MAP.isEmpty()) { // if we no longer have any resources being tracked, // then dispose the lifecycle listener LIFECYCLE_LISTENER.dispose(); LIFECYCLE_LISTENER = null; } } finally { CRITICAL_SECTION.unlock(); } } private final IFile _file; private LifecycleListener _lifecycleListener; private IResourceLifecycleListener _resListener; private volatile boolean _isDisposed; private Map<Object, ISymbol> _requestMap; private Map<Object, ISymbol> _sessionMap; private Map<Object, ISymbol> _applicationMap; private Map<Object, ISymbol> _noneMap; private Map<Object, ISymbol> _viewMap; private Map<Object, ISymbol> _flashMap; // used to avoid infinite recursion in refresh. Must never be null private final CountingMutex _lastModificationStampMonitor = new CountingMutex(); /** * Construct a new JSPModelProcessor for model * * @param model */ private JSPModelProcessor(final IFile file, final LifecycleListener lifecycleListener) { _file = file; _lifecycleListener = lifecycleListener; _resListener = new IResourceLifecycleListener() { public EventResult acceptEvent(final ResourceLifecycleEvent event) { final EventResult result = EventResult.getDefaultEventResult(); // not interested if (!_file.equals(event.getAffectedResource())) { return result; } if (event.getEventType() == EventType.RESOURCE_INACCESSIBLE) { dispose(_file); } else if (event.getEventType() == EventType.RESOURCE_CHANGED) { // if the file has changed contents on disk, then // invoke an unforced refresh of the JSP file if (event.getReasonType() == ReasonType.RESOURCE_CHANGED_CONTENTS && !DISABLE_WKSPACE_CHANGE_REFRESH) { refresh(! FORCE_REFRESH, NO_RUN_AFTER); } } return result; } }; lifecycleListener.addListener(_resListener); } private IDOMModel getModelForFile(final IFile file) throws CoreException, IOException { final IModelManager modelManager = StructuredModelManager.getModelManager(); final IStructuredModel model = modelManager.getModelForRead(file); if (model instanceof IDOMModel) { return (IDOMModel) model; } else if (model != null) { // only release from read if we don't find a DOMModelForJSP // if the model is correct, it will be released in dispose model.releaseFromRead(); } JSFCorePlugin.log("Couldn't get valid model for file: "+file.toString(), new Exception()); //$NON-NLS-1$ return null; } private void dispose() { if (!_isDisposed) { // ensure the resource listener is disposed _lifecycleListener.removeListener(_resListener); _resListener = null; _lifecycleListener = null; if (_requestMap != null) { _requestMap.clear(); _requestMap = null; } if (_sessionMap != null) { _sessionMap.clear(); _sessionMap = null; } if (_applicationMap != null) { _applicationMap.clear(); _applicationMap = null; } if (_noneMap != null) { _noneMap.clear(); _noneMap = null; } // mark as disposed _isDisposed = true; } } /** * @return true if this model processor has been disposed. Disposed * processors should not be used. */ public boolean isDisposed() { return _isDisposed; } /** * If isModelDirty() returns true, then it means that a call * to refresh(false) will trigger a reprocess of the underlying document. * * @return true if the underlying JSP model is considered to be dirty */ public boolean isModelDirty() { final long currentModificationStamp = _file.getModificationStamp(); return _lastModificationStampMonitor.hasChanged(currentModificationStamp); } /** * Refreshes the processor's cache of information from its associated * JSP file. * * @param forceRefresh * @param runAfter */ public void refresh(final boolean forceRefresh, final Runnable runAfter) { if (isDisposed()) { throw new IllegalStateException( "Processor is disposed for file: " + _file.toString()); //$NON-NLS-1$ } if (runAfter == RUN_ON_CURRENT_THREAD) { try { runOnCurrentThread(forceRefresh); } catch (final CoreException e) { JSFCorePlugin.log(e, "Running JSP model processor"); //$NON-NLS-1$ } catch (final OperationCanceledException e) { // ignore } } else { runOnWorkspaceJob(forceRefresh, runAfter); } } private void runOnWorkspaceJob(final boolean forceRefresh, final Runnable runAfter) { WorkspaceJob refreshJob = new WorkspaceJob(NLS.bind(Messages .getString("JSPModelProcessor.0"), _file)) { //$NON-NLS-1$ @Override public IStatus runInWorkspace(IProgressMonitor monitor) throws CoreException { RefreshRunnable runnable = new RefreshRunnable(forceRefresh); runnable.run(monitor); return Status.OK_STATUS; } }; refreshJob.setSystem(true); refreshJob.setRule(ResourcesPlugin.getWorkspace().getRoot()); if (runAfter != null) { refreshJob.addJobChangeListener(new JobChangeAdapter() { @Override public void done(final IJobChangeEvent event) { runAfter.run(); } }); } refreshJob.schedule(); } private void runOnCurrentThread(final boolean forceRefresh) throws CoreException, OperationCanceledException { ResourcesPlugin.getWorkspace().run(new RefreshRunnable(forceRefresh), _file, 0, null); } private final class RefreshRunnable implements IWorkspaceRunnable { private final boolean _forceRefresh; public RefreshRunnable(final boolean forceRefresh) { _forceRefresh = forceRefresh; } public void run(final IProgressMonitor monitor) throws CoreException { synchronized (_lastModificationStampMonitor) { if (!_lastModificationStampMonitor.compareAndSetSignalled(false, true)) { // if this calls succeeds, then this thread has obtained the // lock already and has called through here before. // return immediately to ensure that we don't recurse // infinitely return; } IDOMModel model = null; try { // only refresh if forced or if the underlying file has // changed // since the last run if (_forceRefresh || isModelDirty()) { model = getModelForFile(_file); if (model != null) { refreshInternal(model); } _lastModificationStampMonitor.setModificationStamp(_file.getModificationStamp()); } } catch (final IOException e) { IStatus status = new Status(IStatus.ERROR, JSFCorePlugin.PLUGIN_ID,"Error refreshing internal model", e); //$NON-NLS-1$ final CoreException e2 = new CoreException(status); throw e2; } // make sure that we unsignal the monitor before releasing the // mutex finally { _lastModificationStampMonitor.setSignalled(false); if (model != null) { model.releaseFromRead(); } } } } } private void refreshInternal(final IDOMModel model) { final IStructuredDocumentContext context = IStructuredDocumentContextFactory.INSTANCE.getContext(model.getStructuredDocument(), -1); final ITaglibContextResolver taglibResolver = IStructuredDocumentContextResolverFactory.INSTANCE.getTaglibContextResolver(context); final IDOMDocument document = model.getDocument(); getApplicationMap().clear(); getRequestMap().clear(); getSessionMap().clear(); if (taglibResolver == null) { // unusual, but protect against possible NPE JSFCorePlugin.log(IStatus.ERROR, "Program Error: taglib resolver is null."); //$NON-NLS-1$ return; } //long curTime = System.currentTimeMillis(); recurseChildNodes(model, document.getChildNodes(), taglibResolver); //long netTime = System.currentTimeMillis() - curTime; //System.out.println("Net time to recurse document: "+netTime); } private void recurseChildNodes(final IDOMModel model, final NodeList nodes, final ITaglibContextResolver taglibResolver) { for (int i = 0; i < nodes.getLength(); i++) { final Node child = nodes.item(i); // process attributes at this node before recursing processAttributes(model, child, taglibResolver); recurseChildNodes(model, child.getChildNodes(), taglibResolver); } } private void processAttributes(final IDOMModel model, final Node node, final ITaglibContextResolver taglibResolver) { if (taglibResolver.hasTag(node)) { final String uri = taglibResolver.getTagURIForNodeName(node); final String elementName = node.getLocalName(); for (int i = 0; i < node.getAttributes().getLength(); i++) { final Node attribute = node.getAttributes().item(i); processSymbolContrib(model, uri, elementName, attribute); processSetsLocale(uri, elementName, attribute); } } } private void processSymbolContrib(final IDOMModel model, final String uri, final String elementName, final Node attribute) { final SymbolContribAggregator aggregator = SymbolContribAggregator. create(_file, uri, elementName, attribute.getLocalName()); if (aggregator != null) { final AbstractContextSymbolFactory factory = aggregator.getFactory(); final String symbolName = attribute.getNodeValue(); if (factory != null) { // long curTime = System.currentTimeMillis(); final IStructuredDocumentContext context = IStructuredDocumentContextFactory.INSTANCE. getContext(model.getStructuredDocument(), attribute); if (factory.supports(context)) { final List problems = new ArrayList(); final ISymbol symbol = factory.create(symbolName, ISymbolConstants.SYMBOL_SCOPE_REQUEST, //TODO: context, problems, // TODO: add meta-data for signature new AdditionalContextSymbolInfo(aggregator.getStaticType(), aggregator.getValueExpressionAttr())); // long netTime = System.currentTimeMillis() - curTime; // System.out.println("Time to process loadBundle: "+netTime); if (symbol != null) { updateMap(symbol, aggregator.getScope()); } } } else { final IComponentSymbol componentSymbol = SymbolFactory.eINSTANCE.createIComponentSymbol(); componentSymbol.setName(symbolName); updateMap(componentSymbol, aggregator.getScope()); } } } @SuppressWarnings("deprecation") private void processSetsLocale(final String uri, final String elementName, Node attribute) { LocaleSetAggregator aggregator = LocaleSetAggregator.create(_file, uri, elementName, attribute.getLocalName()); if (aggregator != null) { DesignTimeApplicationManager dtAppMgr = DesignTimeApplicationManager.getInstance(_file.getProject()); if (dtAppMgr != null) { DTFacesContext facesContext = dtAppMgr.getFacesContext(_file); if (facesContext != null) { facesContext.setLocaleString(attribute.getNodeValue()); } } } } /** * @param scopeName - one of "request", "session" or "application" or "view" if JSF2.x * @return an unmodifable map containing all known symbols for * that scope. If scopeName is not found, returns the empty map. */ public Map<Object, ISymbol> getMapForScope(final String scopeName) { final Map<Object, ISymbol> map = getMapForScopeInternal(scopeName); if (map != null) { return Collections.unmodifiableMap(map); } return Collections.EMPTY_MAP; } private void updateMap(final ISymbol symbol, final String scopeName) { final Map<Object, ISymbol> map = getMapForScopeInternal(scopeName); if (map != null) { map.put(symbol.getName(), symbol); } else { Platform.getLog(JSFCorePlugin.getDefault().getBundle()).log(new Status(IStatus.ERROR, JSFCorePlugin.PLUGIN_ID, 0, "Scope not found: "+scopeName, new Throwable())); //$NON-NLS-1$ } } private Map<Object, ISymbol> getMapForScopeInternal(final String scopeName) { if (ISymbolConstants.SYMBOL_SCOPE_REQUEST_STRING.equals(scopeName)) { return getRequestMap(); } else if (ISymbolConstants.SYMBOL_SCOPE_SESSION_STRING.equals(scopeName)) { return getSessionMap(); } else if (ISymbolConstants.SYMBOL_SCOPE_APPLICATION_STRING.equals(scopeName)) { return getApplicationMap(); } else if (ISymbolConstants.SYMBOL_SCOPE_NONE_STRING.equals(scopeName)) { return getNoneMap(); } else if (ISymbolConstants.SYMBOL_SCOPE_VIEW_STRING.equals(scopeName)) { return getViewMap(); } else if (ISymbolConstants.SYMBOL_SCOPE_FLASH_STRING.equals(scopeName)) { return getFlashMap(); } Platform.getLog(JSFCorePlugin.getDefault().getBundle()).log(new Status(IStatus.ERROR, JSFCorePlugin.PLUGIN_ID, 0, "Scope not found: "+scopeName, new Throwable())); //$NON-NLS-1$ return null; } private Map getRequestMap() { if (_requestMap == null) { _requestMap = new HashMap<Object, ISymbol>(); } return _requestMap; } private Map<Object, ISymbol> getSessionMap() { if (_sessionMap == null) { _sessionMap = new HashMap<Object, ISymbol>(); } return _sessionMap; } private Map<Object, ISymbol> getApplicationMap() { if (_applicationMap == null) { _applicationMap = new HashMap<Object, ISymbol>(); } return _applicationMap; } private Map<Object, ISymbol> getNoneMap() { if (_noneMap == null) { _noneMap = new HashMap<Object, ISymbol>(); } return _noneMap; } private Map<Object, ISymbol> getViewMap() { if (_viewMap == null) { _viewMap = new HashMap<Object, ISymbol>(); } return _viewMap; } private Map<Object, ISymbol> getFlashMap() { if (_flashMap == null) { _flashMap = new HashMap<Object, ISymbol>(); } return _flashMap; } /** * Aggregates the sets-locale meta-data * * @author cbateman */ private static class LocaleSetAggregator { private final static String SETS_LOCALE = "sets-locale"; //$NON-NLS-1$ private static LocaleSetAggregator create(final IFile file, final String uri, final String elementName, final String attributeName) { final IMetaDataDomainContext context = MetaDataQueryContextFactory.getInstance().createTaglibDomainModelContext(file); final ITaglibDomainMetaDataQuery query = MetaDataQueryFactory.getInstance().createQuery(context); final Trait trait = query.getQueryHelper().getTrait(uri, elementName+"/"+attributeName, SETS_LOCALE); //$NON-NLS-1$ // final ITaglibDomainMetaDataModelContext mdContext = TaglibDomainMetaDataQueryHelper.createMetaDataModelContext(project, uri); // final Trait trait = TaglibDomainMetaDataQueryHelper.getTrait(mdContext, elementName+"/"+attributeName, SETS_LOCALE); //$NON-NLS-1$ if (TraitValueHelper.getValueAsBoolean(trait)) { return new LocaleSetAggregator(); } return null; } } /** * Aggregates all the symbol contributor meta-data into a single object * * @author cbateman * */ private static class SymbolContribAggregator { private final static String CONTRIBUTES_VALUE_BINDING = "contributes-value-binding"; //$NON-NLS-1$ private final static String VALUE_BINDING_SCOPE = "value-binding-scope"; //$NON-NLS-1$ private final static String VALUE_BINDING_SYMBOL_FACTORY = "value-binding-symbol-factory"; //$NON-NLS-1$ private final static String STATIC_TYPE_KEY = "optional-value-binding-static-type"; //$NON-NLS-1$ private final static String VALUEEXPRESSION_ATTR_NAME_KEY = "optional-value-binding-valueexpr-attr"; //$NON-NLS-1$ /** * @param attributeName * @return a new instance only if attributeName is a symbol contributor */ private static SymbolContribAggregator create(final IFile file, final String uri, final String elementName, final String attributeName) { final String entityKey = elementName+"/"+attributeName; //$NON-NLS-1$ final IMetaDataDomainContext context = MetaDataQueryContextFactory.getInstance().createTaglibDomainModelContext(file); final ITaglibDomainMetaDataQuery query = MetaDataQueryFactory.getInstance().createQuery(context); final Entity entity = query.getQueryHelper().getEntity(uri, entityKey); Trait trait = query.getQueryHelper().getTrait(entity, CONTRIBUTES_VALUE_BINDING); // final ITaglibDomainMetaDataModelContext mdContext = TaglibDomainMetaDataQueryHelper.createMetaDataModelContext(project, uri); // Trait trait = TaglibDomainMetaDataQueryHelper.getTrait(mdContext, entityKey, CONTRIBUTES_VALUE_BINDING); final boolean contribsValueBindings = TraitValueHelper.getValueAsBoolean(trait); if (contribsValueBindings) { String scope = null; String symbolFactory = null; trait = query.getQueryHelper().getTrait(entity, VALUE_BINDING_SCOPE); scope = TraitValueHelper.getValueAsString(trait); if (scope != null && !scope.equals("")) //$NON-NLS-1$ { trait = query.getQueryHelper().getTrait(entity, VALUE_BINDING_SYMBOL_FACTORY); symbolFactory = TraitValueHelper.getValueAsString(trait); } trait = query.getQueryHelper().getTrait(entity, STATIC_TYPE_KEY); String staticType = null; if (trait != null) { staticType = TraitValueHelper.getValueAsString(trait); } trait = query.getQueryHelper().getTrait(entity, VALUEEXPRESSION_ATTR_NAME_KEY); String valueExprAttr = null; if (trait != null) { valueExprAttr = TraitValueHelper.getValueAsString(trait); } return new SymbolContribAggregator(scope, symbolFactory, staticType, valueExprAttr); } return null; } private final Map<String, String> _metadata = new HashMap<String, String>(4); SymbolContribAggregator(final String scope, final String factory, final String staticType, final String valueExprAttr) { _metadata.put("scope", scope); //$NON-NLS-1$ _metadata.put("factory", factory); //$NON-NLS-1$ _metadata.put("staticType", staticType); //$NON-NLS-1$ _metadata.put("valueExprAttr", valueExprAttr); //$NON-NLS-1$ } /** * @return the scope */ public String getScope() { return _metadata.get("scope"); //$NON-NLS-1$ } /** * @return the factory */ public AbstractContextSymbolFactory getFactory() { return JSFCommonPlugin.getSymbolFactories().get(_metadata.get("factory")); //$NON-NLS-1$ } public String getStaticType() { return _metadata.get("staticType"); //$NON-NLS-1$ } public String getValueExpressionAttr() { return _metadata.get("valueExprAttr"); //$NON-NLS-1$ } } private final static class CountingMutex extends Object { private long _lastModificationStamp = -1; private boolean _signalled = false; private final ILock _lock = Job.getJobManager().newLock(); /** * Similar to AtomicBoolean.compareAndSet. If the signalled flag * is the same as expect then update is written to the flag. Otherwise, * nothing happens. * @param expect the value of _signalled where update occurs * @param update the value written to _signalled if _signalled == expect * * @return true if the signalled flag was set to update */ public boolean compareAndSetSignalled(final boolean expect, final boolean update) { final boolean[] value = new boolean[1]; safeRun(new Runnable() { public void run() { if (_signalled == expect) { _signalled = update; value[0] = true; } else { value[0] = false; } }}); return value[0]; } public boolean hasChanged(final long currentModificationStamp) { final boolean[] value = new boolean[1]; safeRun(new Runnable() { public void run() { value[0] = (_lastModificationStamp != currentModificationStamp); }}); return value[0]; } /** * @param signalled */ public void setSignalled(final boolean signalled) { safeRun(new Runnable() { public void run() { _signalled = signalled; }}); } public void setModificationStamp(final long newValue) { safeRun(new Runnable() { public void run() { _lastModificationStamp = newValue; }}); } private void safeRun(final Runnable runnable) { _lock.acquire(); try { runnable.run(); } finally { _lock.release(); } } } }