/**
* (C) Copyright 2013 Jabylon (http://www.jabylon.org) 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
*/
/**
*
*/
package org.jabylon.resources.persistence.internal;
import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import org.apache.felix.scr.annotations.Activate;
import org.apache.felix.scr.annotations.Component;
import org.apache.felix.scr.annotations.Deactivate;
import org.apache.felix.scr.annotations.Reference;
import org.apache.felix.scr.annotations.ReferenceCardinality;
import org.apache.felix.scr.annotations.ReferencePolicy;
import org.apache.felix.scr.annotations.Service;
import org.eclipse.emf.cdo.CDONotification;
import org.eclipse.emf.cdo.CDOObject;
import org.eclipse.emf.cdo.common.CDOCommonSession.Options.PassiveUpdateMode;
import org.eclipse.emf.cdo.common.id.CDOID;
import org.eclipse.emf.cdo.eresource.CDOResource;
import org.eclipse.emf.cdo.net4j.CDONet4jSession;
import org.eclipse.emf.cdo.transaction.CDOTransaction;
import org.eclipse.emf.cdo.util.CommitException;
import org.eclipse.emf.cdo.util.ObjectNotFoundException;
import org.eclipse.emf.cdo.view.CDOAdapterPolicy;
import org.eclipse.emf.cdo.view.CDOView;
import org.eclipse.emf.common.notify.Notification;
import org.eclipse.emf.common.notify.Notifier;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.common.util.URI;
import org.eclipse.emf.ecore.util.EContentAdapter;
import org.eclipse.emf.ecore.util.EcoreUtil;
import org.eclipse.net4j.util.lifecycle.LifecycleUtil;
import org.jabylon.cdo.connector.RepositoryConnector;
import org.jabylon.cdo.server.ServerConstants;
import org.jabylon.properties.Project;
import org.jabylon.properties.ProjectLocale;
import org.jabylon.properties.ProjectVersion;
import org.jabylon.properties.PropertiesPackage;
import org.jabylon.properties.PropertyFile;
import org.jabylon.properties.PropertyFileDescriptor;
import org.jabylon.properties.Workspace;
import org.jabylon.properties.util.PropertiesResourceImpl;
import org.jabylon.resources.changes.PropertiesListener;
import org.jabylon.resources.diff.PropertyDifferentiator;
import org.jabylon.resources.persistence.PropertyPersistenceService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import com.google.common.cache.CacheBuilder;
import com.google.common.cache.LoadingCache;
/**
* @author Johannes Utzig (jutzig.dev@googlemail.com)
*
*/
@Component
@Service(PropertyPersistenceService.class)
public class PropertiesPersistenceServiceImpl implements PropertyPersistenceService, Runnable {
private static final Logger LOGGER = LoggerFactory.getLogger(PropertiesPersistenceServiceImpl.class);
private final class ListeningAdapter extends EContentAdapter {
@Override
public void notifyChanged(Notification notification) {
super.notifyChanged(notification);
if (notification.getNotifier() instanceof ProjectVersion) {
ProjectVersion version = (ProjectVersion) notification.getNotifier();
if (notification.getEventType() == Notification.ADD) {
Object object = (Object) notification.getNewValue();
if (object instanceof ProjectLocale) {
ProjectLocale locale = (ProjectLocale) object;
EList<PropertyFileDescriptor> descriptors = locale.getDescriptors();
for (PropertyFileDescriptor propertyFileDescriptor : descriptors) {
firePropertiesAdded(propertyFileDescriptor, false);
}
}
}
else if (notification.getEventType() == Notification.REMOVE) {
Object object = (Object) notification.getOldValue();
if (object instanceof ProjectLocale) {
ProjectLocale locale = (ProjectLocale) object;
EList<PropertyFileDescriptor> descriptors = locale.getDescriptors();
// FIXME: delete obsolete resource folders and
// derived locale descriptors
// this doesn't work because the object is already invalid
// for (PropertyFileDescriptor propertyFileDescriptor : descriptors) {
// firePropertiesDeleted(propertyFileDescriptor, false);
//
// }
}
}
else if (notification.getEventType() == Notification.SET) {
Object object = (Object) notification.getNewValue();
if (object instanceof ProjectLocale) {
ProjectLocale locale = (ProjectLocale) object;
EList<PropertyFileDescriptor> descriptors = locale.getDescriptors();
for (PropertyFileDescriptor propertyFileDescriptor : descriptors) {
firePropertiesAdded(propertyFileDescriptor, false);
}
}
}
}
if (notification.getNotifier() instanceof ProjectLocale) {
// TODO: delete isn't properly working yet
ProjectLocale locale = (ProjectLocale) notification.getNotifier();
if (notification.getEventType() == Notification.ADD
&& PropertiesPackage.Literals.PROJECT_LOCALE__DESCRIPTORS == notification.getFeature()) {
Object object = notification.getNewValue();
if (object instanceof PropertyFileDescriptor) {
PropertyFileDescriptor descriptor = (PropertyFileDescriptor) object;
firePropertiesAdded(descriptor, false);
}
}
if (notification.getEventType() == Notification.REMOVE
&& PropertiesPackage.Literals.PROJECT_LOCALE__DESCRIPTORS == notification.getFeature()) {
Object object = notification.getOldValue();
if (object instanceof PropertyFileDescriptor) {
PropertyFileDescriptor descriptor = (PropertyFileDescriptor) object;
firePropertiesDeleted(descriptor, false);
}
}
}
if (notification.getNotifier() instanceof PropertyFileDescriptor) {
PropertyFileDescriptor descriptor = (PropertyFileDescriptor) notification.getNotifier();
if (notification.getEventType() == CDONotification.DETACH_OBJECT) {
firePropertiesDeleted(descriptor, false);
}
}
}
@Override
protected void removeAdapter(Notifier notifier) {
if (notifier instanceof CDOObject) {
CDOObject o = (CDOObject) notifier;
if (!o.cdoInvalid())
super.removeAdapter(notifier);
}
}
protected void handleContainment(Notification notification) {
switch (notification.getEventType()) {
// prevent ClassCastExceptions if old value is not a notified,
// but a CDOID
case Notification.REMOVE: {
Object oldValue = notification.getOldValue();
if (oldValue instanceof Notifier) {
Notifier notifier = (Notifier) oldValue;
removeAdapter(notifier);
} else if (oldValue instanceof CDOID) {
CDOID id = (CDOID) oldValue;
removeAdapter(workspace.cdoView().getObject(id));
}
break;
}
case Notification.REMOVE_MANY: {
Collection<?> oldValues = (Collection<?>) notification.getOldValue();
for (Object oldContentValue : oldValues) {
if (oldContentValue instanceof Notifier) {
Notifier notifier = (Notifier) oldContentValue;
removeAdapter(notifier);
} else if (oldContentValue instanceof CDOID) {
CDOID id = (CDOID) oldContentValue;
try {
removeAdapter(workspace.cdoView().getObject(id));
} catch (ObjectNotFoundException e) {
logger.warn("REMOVE_MANY object ID not found: "+notification,e);
}
}
}
break;
}
default:
super.handleContainment(notification);
}
}
}
@Reference(policy = ReferencePolicy.DYNAMIC, cardinality = ReferenceCardinality.OPTIONAL_MULTIPLE, referenceInterface = PropertiesListener.class, bind="addPropertiesListener",unbind="removePropertiesListener")
private List<PropertiesListener> listeners;
private BlockingQueue<PropertyTuple> queue;
private static Logger logger = LoggerFactory.getLogger(PropertiesPersistenceServiceImpl.class);
@Reference
private RepositoryConnector repositoryConnector;
private Workspace workspace;
private Thread runner;
private LoadingCache<CDOID, PropertyFile> cache;
private boolean active;
public PropertiesPersistenceServiceImpl() {
listeners = new CopyOnWriteArrayList<PropertiesListener>();
}
@Activate
public void activate()
{
queue = new ArrayBlockingQueue<PropertyTuple>(50);
runner = new Thread(this, "Properties Persistence Service");
runner.setDaemon(true);
runner.start();
active = true;
}
@Deactivate
public void deactivate()
{
shutdownQueue();
listeners.clear();
queue = null;
runner.interrupt();
runner = null;
}
private void shutdownQueue() {
active = false;
int size = queue.size();
logger.info("Shutting down. Queuesize is {}",size);
while(queue.size()>0) {
logger.info("Shutting down. Remaining Queuesize is {}",queue.size());
try {
Thread.sleep(100);
} catch (InterruptedException e) {
logger.info("Interrupted while draining queue. Exiting");
}
}
}
public void bindRepositoryConnector(RepositoryConnector repositoryConnector) {
this.repositoryConnector = repositoryConnector;
hookListener(repositoryConnector);
}
private void hookListener(final RepositoryConnector repositoryConnector) {
CDONet4jSession session = repositoryConnector.createSession();
session.options().setPassiveUpdateMode(PassiveUpdateMode.ADDITIONS);
CDOView view = repositoryConnector.openView(session);
view.options().addChangeSubscriptionPolicy(CDOAdapterPolicy.ALL);
CDOResource resource = view.getResource(ServerConstants.WORKSPACE_RESOURCE);
workspace = (Workspace) resource.getContents().get(0);
cache = CacheBuilder.newBuilder().expireAfterAccess(10, TimeUnit.MINUTES).concurrencyLevel(5).maximumWeight(50000).weigher(new PropertySizeWeigher()).build(new PropertyFileCacheLoader(repositoryConnector.openView(session)));
//this is very expensive, so don't do it during the bind phase
new Thread(new Runnable() {
@Override
public void run() {
long time = System.currentTimeMillis();
workspace.eAdapters().add(new ListeningAdapter());
LOGGER.info("Installed EContentAdapter in {} seconds",(System.currentTimeMillis()-time)/1000);
}
},"Install Persistence Listener").start();
}
public void unbindRepositoryConnector(RepositoryConnector repositoryConnector) {
this.repositoryConnector = null;
if(workspace!=null) {
CDOView view = workspace.cdoView();
org.eclipse.emf.cdo.session.CDOSession session = view.getSession();
LifecycleUtil.deactivate(view);
LifecycleUtil.deactivate(session);
session = null;
}
}
/*
* (non-Javadoc)
*
* @see org.jabylon.resources.persistence.PropertyPersistenceService#
* saveProperties(org.jabylon.properties.PropertyFileDescriptor,
* org.jabylon.properties.PropertyFile)
*/
@Override
public void saveProperties(PropertyFileDescriptor descriptor, PropertyFile file) {
saveProperties(descriptor, file, false);
}
@Override
public void saveProperties(PropertyFileDescriptor descriptor, PropertyFile file, boolean autoTranslate) {
if(!active)
{
logger.error("Received save request while not active");
throw new IllegalStateException("The PropertiesPersistanceService is deactivated");
}
try {
PropertyFileDescriptor adaptedDescriptor = workspace.cdoView().getObject(descriptor);
PropertyFile writeCopy = createCopy(file);
// create a write copy to be independent of future writes
queue.put(new PropertyTuple(adaptedDescriptor, writeCopy, autoTranslate));
} catch (InterruptedException e) {
throw new RuntimeException("Interrupted while trying to save " + descriptor.fullPath(), e);
}
}
private PropertyFile createCopy(PropertyFile file) {
return EcoreUtil.copy(file);
}
/*
* (non-Javadoc)
*
* @see org.jabylon.resources.persistence.PropertyPersistenceService#
* addPropertiesListener
* (org.jabylon.resources.changes.PropertiesListener)
*/
@Override
public void addPropertiesListener(PropertiesListener listener) {
listeners.add(listener);
}
/*
* (non-Javadoc)
*
* @see org.jabylon.resources.persistence.PropertyPersistenceService#
* removePropertiesListener
* (org.jabylon.resources.changes.PropertiesListener)
*/
@Override
public void removePropertiesListener(PropertiesListener listener) {
listeners.remove(listener);
}
@Override
public void run() {
CDOTransaction transaction = null;
try {
while (true) {
PropertyTuple tuple = queue.take();
try {
if (transaction == null)
transaction = workspace.cdoView().getSession().openTransaction();
PropertyFileDescriptor descriptor = transaction.getObject(tuple.getDescriptor());
PropertyFile file = tuple.getFile();
URI path = descriptor.absolutPath();
Map<String, Object> options = createOptions(descriptor);
if (new File(path.toFileString()).exists()) {
PropertyFile original = descriptor.loadProperties();
PropertyDifferentiator differentiator = new PropertyDifferentiator(original);
PropertiesResourceImpl resource = new PropertiesResourceImpl(path);
resource.getContents().add(file);
resource.save(options);
descriptor.setKeys(resource.getSavedProperties());
descriptor.updatePercentComplete();
transaction.commit();
List<Notification> diff = differentiator.diff(file);
firePropertiesChanges(descriptor, diff, tuple.isAutoSync());
} else {
PropertiesResourceImpl resource = new PropertiesResourceImpl(path);
resource.getContents().add(file);
resource.save(options);
descriptor.setKeys(resource.getSavedProperties());
descriptor.updatePercentComplete();
transaction.commit();
// FIXME: create resource folders and handle derived
// locales
firePropertiesAdded(descriptor, tuple.isAutoSync());
}
} catch (IOException e) {
logger.error("Exception while processing "+tuple,e);
} catch (CommitException e) {
logger.error("failed to commit while processing "+tuple,e);
transaction.close();
transaction = null;
}
}
} catch (InterruptedException e) {
logger.warn("Received Interrupt. Shutting down...");
// let thread end...
} finally {
if(queue!=null)
queue.clear();
if (transaction != null)
transaction.close();
}
}
private Map<String, Object> createOptions(PropertyFileDescriptor descriptor) {
Map<String, Object> options = new HashMap<String, Object>();
if (descriptor.getProjectLocale() != null && descriptor.getProjectLocale().getParent() != null
&& descriptor.getProjectLocale().getParent().getParent() != null) {
ProjectVersion version = descriptor.getProjectLocale().getParent();
Project project = version.getParent();
String propertyType = project.getPropertyType();
options.put(PropertiesResourceImpl.OPTION_FILEMODE, propertyType);
}
return options;
}
private void firePropertiesAdded(PropertyFileDescriptor descriptor, boolean autoSync) {
for (PropertiesListener listener : listeners) {
listener.propertyFileAdded(descriptor, autoSync);
}
}
private void firePropertiesDeleted(PropertyFileDescriptor descriptor, boolean autoSync) {
Iterator<PropertiesListener> it = listeners.iterator();
while (it.hasNext()) {
PropertiesListener listener = it.next();
listener.propertyFileDeleted(descriptor, autoSync);
}
}
private void firePropertiesChanges(PropertyFileDescriptor descriptor, List<Notification> diff, boolean autoSync) {
Iterator<PropertiesListener> it = listeners.iterator();
while (it.hasNext()) {
PropertiesListener listener = it.next();
listener.propertyFileModified(descriptor, diff, autoSync);
}
}
@Override
public PropertyFile loadProperties(PropertyFileDescriptor descriptor) throws ExecutionException {
return loadProperties(descriptor.cdoID());
}
@Override
public PropertyFile loadProperties(CDOID descriptor) throws ExecutionException {
return cache.get(descriptor);
}
@Override
public void clearCache() {
cache.invalidateAll();
}
}
class PropertyTuple {
private PropertyFileDescriptor descriptor;
private PropertyFile file;
private boolean autoSync;
public PropertyTuple(PropertyFileDescriptor descriptor, PropertyFile file, boolean autoSync) {
super();
this.descriptor = descriptor;
this.file = file;
this.autoSync = autoSync;
}
public PropertyFileDescriptor getDescriptor() {
return descriptor;
}
public PropertyFile getFile() {
return file;
}
public boolean isAutoSync() {
return autoSync;
}
@Override
public String toString() {
return "PropertyTuple [descriptor=" + descriptor + ", file=" + file + ", autoSync=" + autoSync + "]";
}
}