/******************************************************************************* * Copyright (c) 2009, 2010 Fraunhofer IWU and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Fraunhofer IWU - initial API and implementation *******************************************************************************/ package net.enilink.komma.model.base; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Set; import java.util.concurrent.CopyOnWriteArraySet; import net.enilink.commons.util.extensions.RegistryFactoryHelper; import net.enilink.composition.properties.PropertySetFactory; import net.enilink.composition.traits.Behaviour; import net.enilink.komma.common.adapter.AdapterSet; import net.enilink.komma.common.adapter.IAdapterFactory; import net.enilink.komma.common.adapter.IAdapterSet; import net.enilink.komma.common.notify.FilterUtil; import net.enilink.komma.common.notify.INotification; import net.enilink.komma.common.notify.INotificationListener; import net.enilink.komma.common.notify.NotificationSupport; import net.enilink.komma.core.BlankNode; import net.enilink.komma.core.EntityVar; import net.enilink.komma.core.IEntityManager; import net.enilink.komma.core.IEntityManagerFactory; import net.enilink.komma.core.IProvider; import net.enilink.komma.core.IReference; import net.enilink.komma.core.IUnitOfWork; import net.enilink.komma.core.KommaException; import net.enilink.komma.core.KommaModule; import net.enilink.komma.core.URI; import net.enilink.komma.dm.IDataManager; import net.enilink.komma.dm.IDataManagerFactory; import net.enilink.komma.dm.change.IDataChange; import net.enilink.komma.dm.change.IDataChangeListener; import net.enilink.komma.dm.change.IDataChangeSupport; import net.enilink.komma.dm.change.IDataChangeTracker; import net.enilink.komma.dm.change.INamespaceChange; import net.enilink.komma.dm.change.IStatementChange; import net.enilink.komma.em.CacheModule; import net.enilink.komma.em.CachingEntityManagerModule; import net.enilink.komma.em.EntityManagerFactoryModule; import net.enilink.komma.em.ThreadLocalDataManager; import net.enilink.komma.em.util.KommaUtil; import net.enilink.komma.model.IContentHandler; import net.enilink.komma.model.IModel; import net.enilink.komma.model.IModelSet; import net.enilink.komma.model.IObject; import net.enilink.komma.model.IURIConverter; import net.enilink.komma.model.ModelPlugin; import net.enilink.komma.model.concepts.ModelSet; import net.enilink.komma.model.event.IStatementNotification; import net.enilink.komma.model.event.NamespaceNotification; import net.enilink.komma.model.event.StatementNotification; import org.eclipse.core.runtime.CoreException; import org.eclipse.core.runtime.IConfigurationElement; import org.eclipse.core.runtime.IExtensionPoint; import org.eclipse.core.runtime.IExtensionRegistry; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.inject.AbstractModule; import com.google.inject.Inject; import com.google.inject.Injector; import com.google.inject.Module; import com.google.inject.Provider; import com.google.inject.Singleton; /** * An extensible model set implementation. * <p> * The following configuration and control mechanisms are provided: * <ul> * <li><b>Resolve</b></li> * <ul> * <li>{@link #delegatedModel(URI, boolean)}</li> * <li>{@link #getObject(URI, boolean)}</li> * </ul> * <li><b>Demand</b></li> * <ul> * <li>{@link #demandCreateModel(URI)}</li> * <li>{@link #demandLoad(IModel)}</li> * <li>{@link #demandLoadHelper(IModel)}</li> * </ul> * </ul> * </p> */ public abstract class ModelSetSupport implements IModelSet.Internal, ModelSet, Behaviour<IModelSet.Internal> { private final static Logger log = LoggerFactory .getLogger(ModelSetSupport.class); /** * Represents the transient state of this resource */ public static class State { /** * The registered adapter factories. * * @see #getAdapterFactories */ protected List<IAdapterFactory> adapterFactories; private IAdapterSet adapterSet; protected IDataChangeTracker dataChangeTracker; /** * The load options. * * @see #getLoadOptions */ protected Map<Object, Object> loadOptions; protected NotificationSupport<INotification> metaDataNotificationSupport = new NotificationSupport<INotification>(); /** * The local resource factory registry. * * @see #getResourceFactoryRegistry */ protected IModel.Factory.Registry modelFactoryRegistry; /** * The parent module with basic concepts and behaviors shared by all * models. */ protected KommaModule module; protected NotificationSupport<INotification> notificationSupport = new NotificationSupport<INotification>(); protected Map<IReference, CopyOnWriteArraySet<INotificationListener<INotification>>> subjectListeners = new HashMap<>(); protected Injector injector; /** * The URI converter. * * @see #getURIConverter */ protected IURIConverter uriConverter; protected volatile IDataManagerFactory dmFactory; protected volatile IEntityManagerFactory emFactory; IDataManagerFactory getDmFactory() { if (dmFactory == null) { synchronized (this) { if (dmFactory == null) { dmFactory = injector .getInstance(IDataManagerFactory.class); } } } return dmFactory; } IEntityManagerFactory getEmFactory() { if (emFactory == null) { synchronized (this) { if (emFactory == null) { emFactory = injector .getInstance(IEntityManagerFactory.class); } } } return emFactory; } void dispose() { if (emFactory != null) { emFactory.close(); emFactory = null; } if (dmFactory != null) { dmFactory.close(); dmFactory = null; } } } protected EntityVar<State> state; @Inject private IUnitOfWork unitOfWork; @Inject private IDataManagerFactory metaDataManagerFactory; @Inject private Injector injector; @Inject private Provider<Locale> locale; protected State state() { synchronized (state) { State s = state.get(); if (s == null) { state.set(s = new State()); } return s; } } @Override public synchronized IAdapterSet adapters() { if (state().adapterSet == null) { state().adapterSet = new AdapterSet(getBehaviourDelegate()); } return state().adapterSet; } @Override public void addListener(INotificationListener<INotification> listener) { state().notificationSupport.addListener(listener); } @Override public void addMetaDataListener( INotificationListener<INotification> listener) { state().metaDataNotificationSupport.addListener(listener); } @Override public void addSubjectListener(IReference subject, INotificationListener<INotification> listener) { CopyOnWriteArraySet<INotificationListener<INotification>> listeners; Map<IReference, CopyOnWriteArraySet<INotificationListener<INotification>>> subjectListeners = state().subjectListeners; synchronized (subjectListeners) { listeners = subjectListeners.get(subject); if (listeners == null) { listeners = new CopyOnWriteArraySet<INotificationListener<INotification>>(); subjectListeners.put(subject, listeners); } } listeners.add(listener); } @Override public void collectInjectionModules(Collection<Module> modules) { modules.add(new CacheModule(BlankNode.generateId())); // ensure that one shared data manager is used throughout the model set modules.add(new AbstractModule() { protected void configure() { bind(IDataManager.class).to(ThreadLocalDataManager.class).in( Singleton.class); } }); modules.add(new EntityManagerFactoryModule(getModule(), new IProvider<Locale>() { @Override public Locale get() { return locale.get(); } }, new CachingEntityManagerModule() { @Override protected Class<? extends PropertySetFactory> getPropertySetFactoryClass() { Class<? extends PropertySetFactory> factoryClass = getBehaviourDelegate() .getPropertySetFactoryClass(); return factoryClass != null ? factoryClass : super .getPropertySetFactoryClass(); } })); } @Override public URI getDefaultGraph() { return null; } @Override public Class<? extends PropertySetFactory> getPropertySetFactoryClass() { return null; } /* * Javadoc copied from interface. */ public IModel createModel(URI uri) { return getBehaviourDelegate().createModel(uri, null); } /* * Javadoc copied from interface. */ public IModel createModel(URI uri, String contentType) { IModel.Factory modelFactory = getModelFactoryRegistry().getFactory(uri, contentType); if (modelFactory != null) { IModel result = modelFactory.createModel(getBehaviourDelegate(), uri); getModels().add(result); return result; } else { return null; } } /** * Creates a new model appropriate for the URI. It is called by * {@link #getModel(URI, boolean) getModel(URI, boolean)} when a URI that * doesn't exist as a model is demand loaded. This implementation simply * calls {@link #createModel(URI, String) createModel(URI)}. Clients may * extend this as appropriate. * * @param uri * the URI of the resource to create. * @return a new resource. * @see #getModel(URI, boolean) */ protected IModel demandCreateModel(URI uri) { return getBehaviourDelegate().createModel(uri, IContentHandler.UNSPECIFIED_CONTENT_TYPE); } /** * Loads the given resource. It is called by * {@link #demandLoadHelper(IModel) demandLoadHelper(IModel)} to perform a * demand load. This implementation simply calls <code>model.</code> * {@link IModel#load(Map) load}({@link #getLoadOptions() getLoadOptions} * ()). Clients may extend this as appropriate. * * @param model * a model that isn't loaded. * @exception IOException * if there are serious problems loading the model. * @see #getModel(URI, boolean) * @see #demandLoadHelper(IModel) */ protected void demandLoad(IModel model) throws IOException { model.load(getLoadOptions()); } /** * Demand loads the given resource using {@link #demandLoad(IModel)} and * {@link KommaException wraps} any {@link IOException} as a runtime * exception. It is called by {@link #getModel(URI, boolean) getModel(URI, * boolean)} to perform a demand load. * * @param model * a model that isn't loaded. * @see #demandLoad(IModel) */ protected void demandLoadHelper(IModel model) { try { demandLoad(model); } catch (IOException exception) { handleDemandLoadException(model, exception); } } @Override public void dispose() { State theState = state.get(); if (theState != null) { getUnitOfWork().end(); theState.dispose(); try { metaDataManagerFactory.close(); } catch (Exception e) { ModelPlugin.log(e); } state.remove(); } } private <K, T> List<T> ensureList(Map<K, List<T>> map, K key) { List<T> list = map.get(key); if (list == null) { list = new ArrayList<T>(); map.put(key, list); } return list; } protected Map<Object, List<INotification>> addNotification( Map<Object, List<INotification>> groupedNotifications, INotification notification, Object target) { if (groupedNotifications == null) { groupedNotifications = new HashMap<Object, List<INotification>>(); } ensureList(groupedNotifications, target).add(notification); return groupedNotifications; } @Override public void fireNotifications( Collection<? extends INotification> notifications) { state().notificationSupport.fireNotifications(notifications); Map<IReference, CopyOnWriteArraySet<INotificationListener<INotification>>> subjectListeners = state().subjectListeners; // notify subject listeners if required synchronized (subjectListeners) { if (subjectListeners.isEmpty()) { return; } } Map<Object, List<INotification>> groupedNotifications = null; for (INotification notification : notifications) { Object subject = notification.getSubject(); boolean notify; synchronized (subjectListeners) { notify = subjectListeners.containsKey(subject); } if (notify) { groupedNotifications = addNotification(groupedNotifications, notification, subject); } // also send notifications for objects of statements if (notification instanceof IStatementNotification) { subject = ((IStatementNotification) notification).getObject(); synchronized (subjectListeners) { notify = subjectListeners.containsKey(subject); } if (notify) { groupedNotifications = addNotification( groupedNotifications, notification, subject); } } } if (groupedNotifications != null) { for (Map.Entry<Object, List<INotification>> entry : groupedNotifications .entrySet()) { Collection<INotificationListener<INotification>> listeners; synchronized (subjectListeners) { listeners = subjectListeners.get(entry.getKey()); } if (listeners != null) { List<INotification> cache = new ArrayList<INotification>(); for (INotificationListener<INotification> listener : listeners) { Collection<INotification> filtered = FilterUtil.select( notifications, listener.getFilter(), cache); if (!filtered.isEmpty()) { listener.notifyChanged(entry.getValue()); } } } } } } /* * Javadoc copied from interface. */ public List<IAdapterFactory> getAdapterFactories() { return state().adapterFactories; } @Override public IDataChangeSupport getDataChangeSupport() { return state().injector.getInstance(IDataChangeSupport.class); } @Override public IDataChangeTracker getDataChangeTracker() { return state().dataChangeTracker; } @Override public Injector getInjector() { return state().injector; } @Override public IDataManagerFactory getDataManagerFactory() { return state().getDmFactory(); } @Override public IEntityManagerFactory getEntityManagerFactory() { return state().getEmFactory(); } /* * Javadoc copied from interface. */ public Map<Object, Object> getLoadOptions() { if (state().loadOptions == null) { state().loadOptions = new HashMap<Object, Object>(); } return state().loadOptions; } @Override public IEntityManager getMetaDataManager() { return getEntityManager(); } /* * Javadoc copied from interface. */ public IModel getModel(URI uri, boolean loadOnDemand) { List<?> result = getMetaDataManager() .createQuery( "SELECT DISTINCT ?m WHERE { ?ms <http://enilink.net/vocab/komma/models#model> ?m }") .setParameter("m", uri).evaluate(IModel.class).toList(); if (!result.isEmpty()) { IModel model = (IModel) result.get(0); if (loadOnDemand && !model.isLoaded()) { demandLoadHelper(model); } return model; } if (loadOnDemand) { IModel model = demandCreateModel(uri); if (model == null) { throw new RuntimeException("Cannot create a model for '" + uri + "'; a registered model factory is needed"); } if (!model.isLoaded()) { demandLoadHelper(model); } return model; } return null; } /* * Javadoc copied from interface. */ public IModel.Factory.Registry getModelFactoryRegistry() { if (state().modelFactoryRegistry == null) { state().modelFactoryRegistry = new ModelFactoryRegistry() { @Override protected IModel.Factory delegatedGetFactory(URI uri, String contentTypeIdentifier) { IModel.Factory.Registry defaultModelFactoryRegistry = ModelPlugin .getDefault().getModelFactoryRegistry(); return convert(getFactory(uri, defaultModelFactoryRegistry .getProtocolToFactoryMap(), defaultModelFactoryRegistry .getExtensionToFactoryMap(), defaultModelFactoryRegistry .getContentTypeToFactoryMap(), contentTypeIdentifier, false)); } @Override protected Map<?, ?> getContentDescriptionOptions() { return getLoadOptions(); } @Override protected IURIConverter getURIConverter() { return getBehaviourDelegate().getURIConverter(); } }; } return state().modelFactoryRegistry; } public KommaModule getModule() { if (state().module == null) { // Attention: Do not use getClass().getClassLoader() here, since // the actual class is a generated behavior and has a class definer // as class loader -> including this module then within modules of // models would cause mixing of "meta-model behaviors" and // "model behaviors". KommaModule module = new KommaModule( ModelSetSupport.class.getClassLoader()); module.includeModule(KommaUtil.getCoreModule()); // load modules which are registered for any namespace IExtensionRegistry registry = RegistryFactoryHelper.getRegistry(); if (registry != null) { IExtensionPoint extensionPoint = registry.getExtensionPoint( ModelPlugin.PLUGIN_ID, "modules"); if (extensionPoint != null) { for (IConfigurationElement cfgElement : extensionPoint .getConfigurationElements()) { String namespace = cfgElement.getAttribute("uri"); if (namespace == null || namespace.trim().isEmpty()) { try { KommaModule extensionModule = (KommaModule) cfgElement .createExecutableExtension("class"); module.includeModule(extensionModule); } catch (CoreException e) { throw new KommaException( "Unable to instantiate extension module", e); } } } } } module.addReadableGraph(getBehaviourDelegate().getDefaultGraph()); state().module = module; } return state().module; } /* * Javadoc copied from interface. */ public IObject getObject(URI uri, boolean loadOnDemand) { IModel model = getModel(uri.trimFragment(), loadOnDemand); if (model != null) { return model.getManager().find(uri, IObject.class); } else { return null; } } public IUnitOfWork getUnitOfWork() { return unitOfWork; } /* * Javadoc copied from interface. */ public IURIConverter getURIConverter() { if (state().uriConverter == null) { state().uriConverter = new ExtensibleURIConverter(); } return state().uriConverter; } /** * Handles the exception thrown during demand load by recording it as an * error diagnostic and throwing a wrapping runtime exception. * * @param model * the model that threw an exception while loading. * @param exception * the exception thrown from the resource while loading. * @see #demandLoadHelper(IModel) */ protected void handleDemandLoadException(IModel model, IOException exception) { String location = model.getURI() == null ? null : model.getURI() .toString(); Exception cause = exception instanceof IModel.IOWrappedException ? (Exception) exception .getCause() : exception; DiagnosticWrappedException wrappedException = new DiagnosticWrappedException( location, cause); if (model.getErrors().isEmpty()) { try { model.getErrors() .add(exception instanceof IModel.IDiagnostic ? (IModel.IDiagnostic) exception : wrappedException); } catch (Exception e) { // exception is not serializable wrappedException = new DiagnosticWrappedException(location, new RuntimeException(cause.getMessage())); model.getErrors().add(wrappedException); } } throw wrappedException; } @Override public Internal create() { List<Module> modules = new ArrayList<Module>(); ((Internal) getBehaviourDelegate()).collectInjectionModules(modules); Injector modelSetInjector = injector.getParent().getParent() .createChildInjector(modules); IModelSet.Internal result = getBehaviourDelegate(); URI metaDataContext = getMetaDataContext(); if (metaDataContext != null) { KommaModule module = new KommaModule(); // reuse module with model concepts and behaviours, but ignore its // graphs module.includeModule(getEntityManager().getFactory().getModule(), false); module.addWritableGraph(metaDataContext); module.addReadableGraph(metaDataContext); IEntityManager newMetaDataManager = modelSetInjector .getInstance(IEntityManagerFactory.class) .createChildFactory(module).get(); // merge data (rdf:type, etc.) into other repository result = newMetaDataManager.merge(result); } ((IModelSet.Internal) result).init(modelSetInjector); // create a model for the meta data context if (metaDataContext != null) { result.createModel(metaDataContext); } return result; } /** * Initializes the injector that is used within models. */ public void init(Injector injector) { state().injector = injector; setDataChangeTracker(injector.getInstance(IDataChangeTracker.class)); } @Override public void removeListener(INotificationListener<INotification> listener) { state().notificationSupport.removeListener(listener); } @Override public void removeMetaDataListener( INotificationListener<INotification> listener) { state().metaDataNotificationSupport.removeListener(listener); } @Override public void removeSubjectListener(IReference subject, INotificationListener<INotification> listener) { Map<IReference, CopyOnWriteArraySet<INotificationListener<INotification>>> subjectListeners = state().subjectListeners; CopyOnWriteArraySet<INotificationListener<INotification>> listeners; synchronized (subjectListeners) { listeners = subjectListeners.get(subject); } if (listeners != null) { listeners.remove(listener); } } public void setDataChangeTracker(IDataChangeTracker changeTracker) { state().dataChangeTracker = changeTracker; state().dataChangeTracker.addChangeListener(new IDataChangeListener() { URI metaDataContext = getMetaDataContext(); @Override public void dataChanged(List<IDataChange> changes) { ModelSetSupport.this .fireNotifications(transformChanges(changes)); Set<IReference> changedModels = new HashSet<IReference>(); for (IDataChange change : changes) { if (change instanceof IStatementChange) { IReference context = ((IStatementChange) change) .getStatement().getContext(); if (context != null) { changedModels.add(context); } } } for (IReference changedModel : changedModels) { if (changedModel.getURI() != null && !changedModel.getURI().equals(metaDataContext)) { IModel model = getModel(changedModel.getURI(), false); if (model != null && model.isLoaded()) { model.setModified(true); } } } } }); } @Inject protected void setMetaDataChangeTracker(IDataChangeTracker changeTracker) { changeTracker.addChangeListener(new IDataChangeListener() { @Override public void dataChanged(List<IDataChange> changes) { state().metaDataNotificationSupport .fireNotifications(transformChanges(changes)); } }); } /* * Javadoc copied from interface. */ public void setModelFactoryRegistry( IModel.Factory.Registry modelFactoryRegistry) { state().modelFactoryRegistry = modelFactoryRegistry; } /* * Javadoc copied from interface. */ public void setURIConverter(IURIConverter uriConverter) { state().uriConverter = uriConverter; } /** Transforms changes tracked in the repository into {@link INotification}s */ protected List<INotification> transformChanges(List<IDataChange> changes) { List<INotification> notifications = new ArrayList<INotification>( changes.size()); for (IDataChange change : changes) { if (change instanceof INotification) { notifications.add((INotification) change); } else if (change instanceof INamespaceChange) { INamespaceChange nsChange = (INamespaceChange) change; notifications .add(new NamespaceNotification(nsChange.getPrefix(), nsChange.getOldNS(), nsChange.getNewNS())); } else { IStatementChange stmtChange = (IStatementChange) change; notifications.add(new StatementNotification( getBehaviourDelegate(), stmtChange.isAdd(), stmtChange .getStatement())); } } return notifications; } }