/* license-start
*
* Copyright (C) 2008 - 2013 Crispico, <http://www.crispico.com/>.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details, at <http://www.gnu.org/licenses/>.
*
* Contributors:
* Crispico - Initial API and implementation
*
* license-end
*/
package org.flowerplatform.editor.remote;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import org.flowerplatform.common.log.AuditDetails;
import org.flowerplatform.common.log.LogUtil;
import org.flowerplatform.common.util.RunnableWithParam;
import org.flowerplatform.communication.CommunicationPlugin;
import org.flowerplatform.communication.channel.CommunicationChannel;
import org.flowerplatform.communication.channel.ICommunicationChannelLifecycleListener;
import org.flowerplatform.communication.command.DisplaySimpleMessageClientCommand;
import org.flowerplatform.communication.stateful_service.IStatefulClientLocalState;
import org.flowerplatform.communication.stateful_service.NamedLockPool;
import org.flowerplatform.communication.stateful_service.RemoteInvocation;
import org.flowerplatform.communication.stateful_service.StatefulService;
import org.flowerplatform.communication.stateful_service.StatefulServiceInvocationContext;
import org.flowerplatform.editor.EditorPlugin;
import org.flowerplatform.editor.UnlockEditableResourceRunnable;
import org.flowerplatform.editor.collaboration.CollaborativeFigureModel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @author Cristi
*
*/
public abstract class EditorStatefulService extends StatefulService implements IEditorStatefulClientMXBean, ICommunicationChannelLifecycleListener {
private String editorName;
/**
*
*/
private static final Logger logger = LoggerFactory.getLogger(EditorStatefulService.class);
public static final String SUBSCRIBE_ER_AUDIT_CATEGORY = "SUBSCRIBE_ER";
public static final String SAVE_ER_AUDIT_CATEGORY = "SAVE_ER";
/**
* The open {@link EditableResource}. They are mapped by <code>editableResourcePath</code>.
*
* <p>
* We use {@link ConcurrentHashMap} as implementation. This allows access from multiple threads
* without locking. While this is good for performance, it may be bad for synchronization. However,
* the use of {@link NamedLockPool} overcomes this issue.
*
* TODO CS/STFL cred ca dupa mutare runnable, putem pune inapoi protected
*
*/
public Map<String, EditableResource> editableResources = new ConcurrentHashMap<String, EditableResource>();
/**
* We use this instead of normal locking, because, during subscription there is a small
* time window where 2 threads subscribing for the same resource could create the {@link EditableResource}
* twice. Of course, we could have locked on the entire map, which would have solved this, but with
* a big performance impact.
*
* @see NamedLockPool
* @see #subscribe()
* TODO CS/STFL cred ca dupa mutare runnable, putem pune inapoi protected
*
*/
public NamedLockPool namedLockPool = new NamedLockPool();
/**
* Number of seconds until a lock expires. This property is transferred
* at startup to the client, by {@link InitializeEditorSupportClientCommand}.
*
*
*/
public int lockLeaseSeconds = 10;
/**
* Used to schedule unlock operations to be executed when lock expires.
*
*
*/
private ScheduledExecutorService scheduler = CommunicationPlugin.getInstance().getScheduledExecutorServiceFactory().createScheduledExecutorService();
protected AtomicInteger collaborativeFigureModelsIdFactory = new AtomicInteger(1);
private RunnableWithParam<Void, EditableResource> standardRemoveEditableResourceRunnable = new RunnableWithParam<Void, EditableResource>() {
@Override
public Void run(EditableResource editableResource) {
editableResources.remove(editableResource.getEditorInput());
return null;
}
};
private class WebCommunicationChannelAndEditableResources {
private CommunicationChannel webCommunicationChannel;
private List<EditableResource> editableResources = new ArrayList<EditableResource>();
}
///////////////////////////////////////////////////////////////
// JMX Methods
///////////////////////////////////////////////////////////////
@Override
public Collection<String> getStatefulClientIdsForCommunicationChannel(
CommunicationChannel communicationChannel) {
Collection<String> result = new HashSet<String>();
// build the inverse hierarchy
for (EditableResource er : editableResources.values()) {
namedLockPool.lock(er.getEditableResourcePath());
try {
for (EditableResourceClient erc : er.getClients()) {
if (communicationChannel.equals(erc.getCommunicationChannel())) {
result.add(erc.getStatefulClientId());
}
}
} finally {
namedLockPool.unlock(er.getEditableResourcePath());
}
}
return result;
}
public Map<String, EditableResource> getEditableResources() {
return Collections.unmodifiableMap(editableResources);
}
/**
*
*/
@Override
public String printStatefulDataPerCommunicationChannel(String webCommunicationChannelIdFilter, String linePrefix) {
// clean parameters
if ("".equals(webCommunicationChannelIdFilter) || "String".equals(webCommunicationChannelIdFilter)) {
webCommunicationChannelIdFilter = null;
}
if ("String".equals(linePrefix)) {
linePrefix = "";
}
StringBuffer sb = new StringBuffer();
Map<Object, WebCommunicationChannelAndEditableResources> map = new HashMap<Object, EditorStatefulService.WebCommunicationChannelAndEditableResources>();
// build the inverse hierarchy
for (EditableResource er : editableResources.values()) {
namedLockPool.lock(er.getEditableResourcePath());
try {
for (EditableResourceClient erc : er.getClients()) {
// execute if no filter or if filter matches
if (webCommunicationChannelIdFilter == null || webCommunicationChannelIdFilter.equals(erc.getCommunicationChannel().getId())) {
// find or create entry
WebCommunicationChannelAndEditableResources entry = map.get(erc.getCommunicationChannel().getId());
if (entry == null) {
entry = new WebCommunicationChannelAndEditableResources();
entry.webCommunicationChannel = erc.getCommunicationChannel();
map.put(erc.getCommunicationChannel().getId(), entry);
}
// add resource to the list
entry.editableResources.add(er);
}
}
} finally {
namedLockPool.unlock(er.getEditableResourcePath());
}
}
// print
for (WebCommunicationChannelAndEditableResources entry : map.values()) {
sb.append(linePrefix).append(entry.webCommunicationChannel).append("\n");
for (EditableResource er : entry.editableResources) {
sb.append(linePrefix).append(" ").append(er).append("\n");
}
}
return sb.toString();
}
/**
*
*/
@Override
public String printEditableResources() {
StringBuffer sb = new StringBuffer();
for (EditableResource er : editableResources.values()) {
// TODO CS/STFL de scos .toString()
namedLockPool.lock(er.getEditorInput().toString());
try {
sb.append(er).append("\n");
for (EditableResourceClient erc : er.getClients()) {
sb.append(" ").append(erc).append("\n");
}
} finally {
// TODO CS/STFL de scos .toString()
namedLockPool.unlock(er.getEditorInput().toString());
}
}
return sb.toString();
}
/**
* @author Mariana
*
*/
@Override
public void reloadEditableResource(String editableResourcePath, boolean displayMessageToClient) {
EditableResource editableResource = editableResources.get(editableResourcePath);
if (editableResource != null) {
reloadEditableResource(editableResource, displayMessageToClient);
} else {
throw new RuntimeException(String.format("Path %s is not correct!", editableResourcePath));
}
}
/**
*
*/
public int getNumberOfRegisteredLocksForNamedLockPool() {
return namedLockPool.getNumberOfRegisteredLocks();
}
/**
*
*/
@Override
public void unsubscribeClientForcefully(String communicationChannelId, String editableResourcePath) {
EditableResource er = editableResources.get(editableResourcePath);
if (er == null) {
throw new RuntimeException("ER not found for path = " + editableResourcePath);
}
EditableResourceClient clientFound = null;
for (EditableResourceClient client : er.getClients()) {
if (communicationChannelId.equals(client.getCommunicationChannel().getId())) {
clientFound = client;
break;
}
}
if (clientFound == null) {
throw new RuntimeException("Client not found for this ER, for id = " + communicationChannelId);
}
unsubscribeClientForcefully(clientFound, editableResourcePath);
}
/**
*
*/
@Override
public void subscribeClientForcefully(String communicationChannelId, String editableResourcePath) {
CommunicationChannel channel = CommunicationPlugin.getInstance().getCommunicationChannelManager().getCommunicationChannelById(communicationChannelId);
if (channel == null) {
throw new IllegalArgumentException("WebCommunicationChannel not found for id = " + communicationChannelId);
}
subscribeClientForcefully(channel, editableResourcePath);
}
///////////////////////////////////////////////////////////////
// Normal methods
///////////////////////////////////////////////////////////////
/**
*
*/
public String getEditorName() {
return editorName;
}
public void setEditorName(String editorName) {
this.editorName = editorName;
}
/**
* Has the same behavior like the similar method from AS <code>BasicEditorDescriptor</code>.
*
*/
public String calculateStatefulClientId(String editableResourcePath) {
// only one / because the path already contains a /
return editorName + ":/" + editableResourcePath;
}
// /**
// * Should return the ID of this service (as registered
// * in {@link ServiceRegistry}.
// *
// *
// */
// public abstract String getServiceId();
/**
* Should return <code>true</code> if the client applies the modification
* immediately without waiting for server's response (e.g. text files).
* <code>false</code> otherwise (e.g. diagrams).
*
* @see method with same name from AS Class <code>EditorFrontendController</code>.
*
*
*/
protected abstract boolean areLocalUpdatesAppliedImmediately();
/**
* Factory method. Should return a new (unpopulated) {@link EditableResource} instance
* handled by this class.
*
*
*/
protected abstract EditableResource createEditableResourceInstance();
/**
* Invokes {@link #loadEditableResource()}, being exception proof. If the resource is slave => we add
* it to the master.
*
* @return <code>false</code> if an error is thrown while loading.
* @see #disposeEditableResourceSafe()
*
* @author Cristi
* @author Mariana
*
*/
private boolean loadEditableResourceSafe(StatefulServiceInvocationContext context, EditableResource editableResource) {
logger.debug("Loading Editable Resource = {}", editableResource.getEditableResourcePath());
// TODO CS/STFL de reactivat logica de stergere lista cand nu mai avem slave: cand vom face lock si la Master cand operam pe slave
// if (editableResource.getMasterEditableResource().getSlaveEditableResources() == null) {
// editableResource.getMasterEditableResource().setSlaveEditableResources(new ArrayList<EditableResource>());
// }
try {
loadEditableResource(context, editableResource);
} catch (Throwable e) {
if (context != null) {
context.getCommunicationChannel().appendCommandToCurrentHttpResponse(new DisplaySimpleMessageClientCommand(
"Error on Load Resource", String.format("The resource %s cannot be open; it may be corrupted. The following error was encountered: %s", editableResource.getEditableResourcePath(), e), DisplaySimpleMessageClientCommand.ICON_ERROR));
}
logger.error(String.format("Error while loading resource %s", editableResource.getEditableResourcePath()), e);
return false;
}
// Mariana: adding the new resource to the list of slaves here, since removing it is done at dispose()
// this is to ensure that the slaves do not lose their master in case of reloading
if (editableResource.getMasterEditableResource() != null) {
editableResource.getMasterEditableResource().getSlaveEditableResources().add(editableResource);
}
return true;
}
/**
* Should load the content of the {@link EditableResource} (e.g. from the disk) and store
* it in the {@link EditableResource} object.
*
* <p>
* Should throw {@link Exception} if the resource (file, diagram, etc) is not found.
* If it encounters issues while loading, it should throw another type of exception.
*
*
*/
protected abstract void loadEditableResource(StatefulServiceInvocationContext context, EditableResource editableResource);
/**
* Invokes {@link #disposeEditableResource()}, being exception proof. If the resource is slave => we remove
* it from the master.
*
* @see #loadEditableResourceSafe()
*
*/
private void disposeEditableResourceSafe(EditableResource editableResource) {
logger.debug("Disposing Editable Resource = {}", editableResource.getEditableResourcePath());
if (editableResource.getMasterEditableResource() != null) {
editableResource.getMasterEditableResource().getSlaveEditableResources().remove(editableResource);
}
try {
disposeEditableResource(editableResource);
} catch (Throwable e) {
// we catch it and we let the normal logic of the caller continue
logger.error(String.format("Error while disposing resource %s. This may lead to memory leak.", editableResource.getEditableResourcePath()), e);
}
}
/**
* Should do cleanup for the resource.
*
*/
protected abstract void disposeEditableResource(EditableResource editableResource);
/**
* Reloads an {@link EditableResource} by first disposing and then loading it. If the resource does not exist
* anymore (i.e. after reloading a model file, some diagrams might have been deleted), clients are unsubscribed
* forcefully from it.
*
* <p>
* If <code>displayMessageToClient</code>, messages are sent to the subscribed clients, to notify about the resource
* being reloaded or removed.
*
* <p>
* <b>NOTE</b>: This method delegates to {@link #reloadEditableResource_disposeWithSlaves()} that disposes the master + slaves,
* and after that delegates to {@link #reloadEditableResource_loadWithSlaves()}, that loads the master + slaves. If we did otherwise
* (i.e. master dispose + load, for each slave: dispose + load), when processing slaves, they would be in a state where they still reference the old
* content, but their master references the new content. This might lead to subtle issues, difficult to track.
*
* @author Mariana
* @author Cristi
*
*/
protected void reloadEditableResource(EditableResource editableResource, boolean displayMessageToClient) {
namedLockPool.lock(editableResource.getEditableResourcePath());
try {
List<EditableResource> slaveEditableResourcesThatWereUnloaded = reloadEditableResource_disposeWithSlaves(editableResource);
reloadEditableResource_loadWithSlaves(editableResource, slaveEditableResourcesThatWereUnloaded, displayMessageToClient);
} finally {
namedLockPool.unlock(editableResource.getEditableResourcePath());
editableResource.updateLastModifiedStamp();
}
}
/**
* @see #reloadEditableResource()
*
*/
protected List<EditableResource> reloadEditableResource_disposeWithSlaves(EditableResource editableResource) {
List<EditableResource> slaveEditableResources = null;
if (editableResource.getSlaveEditableResources() != null) {
// create a copy here; the lines below will empty this list, so a copy is necessary
slaveEditableResources = new ArrayList<EditableResource>(editableResource.getSlaveEditableResources());
}
for (EditableResourceClient client : editableResource.getClients()) {
doOnClientUnsubscribed(editableResource, client);
}
disposeEditableResourceSafe(editableResource);
if (slaveEditableResources != null) {
for (EditableResource slave : slaveEditableResources) {
slave.getEditorStatefulService().reloadEditableResource_disposeWithSlaves(slave);
}
}
return slaveEditableResources;
}
/**
* @see #reloadEditableResource()
*
*/
protected void reloadEditableResource_loadWithSlaves(EditableResource editableResource, List<EditableResource> slaveEditableResources, boolean displayMessageToClient) {
if (!loadEditableResourceSafe(null, editableResource)) {
unsubscribeAllClientsForcefully(editableResource.getEditableResourcePath(), true);
}
for (EditableResourceClient client : editableResource.getClients()) {
doOnClientSubscribed(editableResource, client);
if (displayMessageToClient) {
client.getCommunicationChannel().appendOrSendCommand(
new DisplaySimpleMessageClientCommand(
"Info",
"Resource " + editableResource.getLabel() + " has been modified on the file system, thus the corresponding editor(s) have been refreshed.",
DisplaySimpleMessageClientCommand.ICON_INFORMATION));
}
}
if (slaveEditableResources != null) {
slaveEditableResources = new ArrayList<EditableResource>(slaveEditableResources);
for (EditableResource slave : slaveEditableResources) {
slave.getEditorStatefulService().reloadEditableResource_loadWithSlaves(slave, null, displayMessageToClient);
}
}
}
/**
*
*/
public EditableResource getEditableResource(String editableResourcePath) {
return editableResources.get(editableResourcePath);
}
/**
* Triggers the save custom logic (i.e. {@link #doSaveEditableResource(EditableResource)}). If
* the dirty state changes (i.e. normal behavior), the {@link EditableResource} will be dispatched
* towards the clients.
*
* <p>
* If the {@link EditableResource} is slave, a WARNING is logged and we delegate to it's master. This is not
* normal, because the client always takes care to send the master (for optimization purposes).
*
*
*/
public void save(StatefulServiceInvocationContext context, String editableResourcePath) {
AuditDetails auditDetails = new AuditDetails(logger, SAVE_ER_AUDIT_CATEGORY, getFriendlyEditableResourcePath(calculateStatefulClientId(editableResourcePath)));
namedLockPool.lock(editableResourcePath);
try {
EditableResource editableResource = editableResources.get(editableResourcePath);
if (editableResource == null) {
logger.error("The Editable Resource with path = {} was not found", editableResourcePath);
return;
}
// if (editableResource.getMasterEditableResource() != null) {
// logger.warn("Save command arrived directly on slave editable resource = {}. Client should have called for the master editable resource. Delegating to master.", editableResource.getEditorInput());
// EditorBackend masterEditorBackend = editableResource.getMasterEditableResource().getEditorBackend();
// masterEditorBackend.saveEditableResource(context, editableResource.getMasterEditableResource().getEditorInput());
// } else {
boolean initialDirtyState = editableResource.isDirty();
doSave(editableResource);
if (initialDirtyState != editableResource.isDirty() && !context.getCommunicationChannel().isDisposed()) {
editableResource.updateLastModifiedStamp();
dispatchEditableResourceStatus(editableResource);
}
// }
// TODO CS/FP2 dezact act listener
// SingletonRefsInEditorPluginFromWebPlugin.INSTANCE_ACTIVITY_LISTENER.updateLastSave(getFriendlyEditableResourcePath(calculateStatefulClientId(editableResourcePath)));
} finally {
namedLockPool.unlock(editableResourcePath);
LogUtil.audit(auditDetails);
}
}
/**
* Must be implemented by extending classes. It is responsible with saving
* the {@link EditableResource}. Slave {@link EditableResource} should throw an
* {@link UnsupportedOperationException}.
*
*
*/
protected abstract void doSave(EditableResource editableResource);
/**
* Sends the given content to the client (using an {@link UpdateContentClientCommand}).
* When the server logic needs to send content to the client, it should always use this method
* (and not to send data directly to the client).
*
*
*/
public void sendContentUpdateToClient(EditableResource editableResource, EditableResourceClient client, Object content, boolean isFullContent) {
if (logger.isTraceEnabled()) {
logger.trace("For Editable Resource = {}, sending to client = {} content = {}; isFullContent = {}", new Object[] { editableResource.getEditableResourcePath(), client.getCommunicationChannel(), content, isFullContent });
}
invokeClientMethod(client.getCommunicationChannel(), client.getStatefulClientId(), "updateContent", new Object[] { content, isFullContent });
}
/**
* Should send the initial content to a client (i.e. when a new client is connected).
* Should use {@link #sendContentUpdateToClient()}.
*
*
*/
protected abstract void sendFullContentToClient(EditableResource editableResource, EditableResourceClient client);
/**
* Should apply the updates to the {@link EditableResource}, and dispatch them
* to the existing clients (using {@link #sendContentUpdateToClient()}.
*
*
*/
protected abstract void updateEditableResourceContentAndDispatchUpdates(StatefulServiceInvocationContext context, EditableResource editableResource, Object updatesToApply);
@Override
public void communicationChannelCreated(
CommunicationChannel webCommunicationChannel) {
// do nothing
}
/**
*
*/
@Override
public void communicationChannelDestroyed(
CommunicationChannel webCommunicationChannel) {
// because the map is a ConcurrentHashMap (and not a synchronized map), means that during
// this iteration other threads might add and remove.
// If they add entries, we are not impacted here: for sure newly added EditableResources
// won't have this channel among the clients, because
// this channel has just been destroyed
for (final Iterator<EditableResource> iter = editableResources.values().iterator(); iter.hasNext(); ) {
EditableResource er = iter.next();
EditableResourceClient erc = er.getEditableResourceClientByCommunicationChannelThreadSafe(webCommunicationChannel);
if (erc != null) {
// now other threads may operate on the resource: add client, update content/dispatch, remove client.
// But we are sure that the resource won't disappear from the editableResources map, because there
// is at least this client/communication channel preventing its removal
// no need to lock, because unsubscribe() locks anyway
// TODO CS/STFL de scos .toString()
unsubscribeInternal(webCommunicationChannel, er.getEditorInput().toString(), new RunnableWithParam<Void, EditableResource>() {
@Override
public Void run(EditableResource param) {
// by using iterator.remove(),
// this implementation of "remove" makes sure that the map won't complain about
// "concurrent modification exception" (i.e. because while iterating on it, we
// remove an element). However, from what I've read, ConcurrentHashMap doesn't throw
// this.
// But anyway, we are safe with this version, and we'll be safe with another Map implementation.
iter.remove();
return null;
}
}, false);
}
}
}
/**
* This is NOT thread safe.
*
* @param clientInvocationOptions TODO
*/
public void dispatchEditableResourceStatus(EditableResource editableResource) {
for (EditableResourceClient client : editableResource.getClients()) {
if (logger.isTraceEnabled()) {
logger.trace(
"For Editable Resource = {}, sending updated Editable Resource Status to client = {}",
editableResource.getEditableResourcePath(),
client.getCommunicationChannel());
}
invokeClientMethod(client.getCommunicationChannel(), client.getStatefulClientId(), "updateEditableResourceStatus", new Object[] { editableResource });
}
}
/**
* This is NOT thread safe. Should be called AFTER adding the new <code>client</code> in the list.
*
* <p>
* Sends the full clients list to the <code>client</code> (all clients without itself). Sends <code>client</code> to the other
* existing clients.
*
*
*/
protected void dispatchClientSubscribedToEditableResource(EditableResource editableResource, EditableResourceClient client) {
if (logger.isTraceEnabled()) {
logger.trace("For Editable Resource = {}, dispatching client added notification; added client = {}", editableResource.getEditableResourcePath(), client.getCommunicationChannel());
}
if (editableResource.getClients().size() <= 1) {
return;
}
// build the list without the calling client
List<EditableResourceClient> existingClients = new ArrayList<EditableResourceClient>(editableResource.getClients().size() - 1);
for (EditableResourceClient otherClient : editableResource.getClients()) {
if (!client.equals(otherClient)) { // will skip the currentClient
existingClients.add(otherClient);
}
}
invokeClientMethod(client.getCommunicationChannel(), client.getStatefulClientId(), "newClientsAdded", new Object[] { existingClients, true });
List<EditableResourceClient> addedClient = Collections.singletonList(client);
for (EditableResourceClient otherClient : existingClients) {
if (logger.isTraceEnabled()) {
logger.trace(
"For Editable Resource = {}, sending client added notification, to client = {}. Added client = {}",
new Object[] {
editableResource.getEditableResourcePath(),
otherClient.getCommunicationChannel(),
client.getCommunicationChannel()
});
}
invokeClientMethod(otherClient.getCommunicationChannel(), otherClient.getStatefulClientId(), "newClientsAdded", new Object[] { addedClient });
}
}
/**
* This is NOT thread safe. Should be called AFTER removing the <code>client</code> from the list.
*
* <p>
* Sends <code>client</code> to the other existing clients.
*
*
*/
public void dispatchClientUnsubscribedFromEditableResource(EditableResource editableResource, EditableResourceClient client) {
if (logger.isTraceEnabled()) {
logger.trace("For Editable Resource = {}, dispatching client removed notification; removed client = {}", editableResource.getEditableResourcePath(), client.getCommunicationChannel());
}
for (EditableResourceClient otherClient : editableResource.getClients()) {
if (logger.isTraceEnabled()) {
logger.trace(
"For Editable Resource = {}, sending client removed notification to client = {}. Removed client = {}",
new Object[] {
editableResource.getEditableResourcePath(),
otherClient.getCommunicationChannel(),
client.getCommunicationChannel()
});
}
invokeClientMethod(otherClient.getCommunicationChannel(), otherClient.getStatefulClientId(), "clientRemoved", new Object[] { client.getCommunicationChannel().getId() });
}
}
/**
* This is NOT thread safe.
*
* <p>
* If the lock is successful (i.e. resource not locked, or locked by the same client), returns
* <code>true</code>. If the lock is acquired, then a notification is sent to the client; if the
* lock is extended, no notification is sent.
*
* <p>
* If the lock is not successful, returns false and sends to the client a {@link DisplaySimpleMessageClientCommand}
* with a message.
*
*
*/
protected boolean tryLock(EditableResource editableResource, EditableResourceClient client) {
if (!editableResource.isLocked() || client.equals(editableResource.getLockOwner())) {
Calendar calendar = Calendar.getInstance();
editableResource.setLockUpdateTime(calendar.getTime());
calendar.add(Calendar.SECOND, lockLeaseSeconds);
editableResource.setLockExpireTime(calendar.getTime());
if (logger.isTraceEnabled()) {
logger.trace("Lock acquired for Editable Resource = {}, by client = {} until {}", new Object[] { editableResource.getEditableResourcePath(), client.getCommunicationChannel(), editableResource.getLockExpireTime()});
}
if (!editableResource.isLocked()) {
// first lock => schedule the runnable
editableResource.setLocked(true);
editableResource.setLockOwner(client);
scheduler.schedule(new UnlockEditableResourceRunnable(this, editableResource.getEditorInput(), client, scheduler), lockLeaseSeconds, TimeUnit.SECONDS);
}
// else we are in the case: renew the lock by the lock owner;
// when scheduled runnable will be triggered, it will reschedule according to the new time
}
boolean lockByCurrentClient = client.equals(editableResource.getLockOwner());
// TODO CS/STFL dezactivat pt. lansare caci face pb la model
// if (!lockByCurrentClient) {
// // locking operation did not succeed; sending a message to the user
// if (logger.isTraceEnabled()) {
// logger.debug("Lock failed for Editable Resource = {}, client = {}. Sending full content...", editableResource.getEditorInput(), client.getCommunicationChannel());
// }
// DisplaySimpleMessageCommand command = new DisplaySimpleMessageCommand("Warning", String.format("Locking '%s' has failed. A few changes done by you may be reverted.", editableResource.getLabel()), DisplaySimpleMessageCommand.ICON_WARNING);
// client.getCommunicationChannel().sendObject(command);
// }
return lockByCurrentClient;
}
/**
* Delegate to {@link #subscribeClientForcefully(CommunicationChannel, String, boolean)} with <code>handleAsClientSubscription</code> <code>false</code>.
*
* @author Mariana
*/
public EditableResource subscribeClientForcefully(CommunicationChannel communicationChannel, String editableResourcePath) {
return subscribeClientForcefully(communicationChannel, editableResourcePath, false);
}
/**
*
*/
public EditableResource subscribeClientForcefully(CommunicationChannel communicationChannel, String editableResourcePath, boolean handleAsClientSubscription) {
EditorStatefulClientLocalState state = new EditorStatefulClientLocalState();
state.setEditableResourcePath(editableResourcePath);
state.setForcingSubscriptionFromServer(true);
state.setHandleAsClientSubscription(handleAsClientSubscription);
subscribe(new StatefulServiceInvocationContext(communicationChannel, null, null), state);
return getEditableResource(editableResourcePath);
}
/**
*
*/
public void unsubscribeClientForcefully(EditableResourceClient client, String editableResourcePath) {
logger.debug("Unsubscribing forcefully from Editable Resource = {}, client = {}", editableResourcePath, client.getCommunicationChannel());
namedLockPool.lock(editableResourcePath);
try {
if (!client.getCommunicationChannel().isDisposed()) { // e.g. the channel is disposed when a client leaves, that has a model + diagram open
invokeClientMethod(client.getCommunicationChannel(), client.getStatefulClientId(), "unsubscribedForcefully", null);
}
unsubscribeInternal(client.getCommunicationChannel(), editableResourcePath, standardRemoveEditableResourceRunnable, false);
} finally {
namedLockPool.unlock(editableResourcePath);
}
}
/**
* Unsubscribes all the clients from the {@link EditableResource}.
*
* <p>
* If <code>displayMessageToClient</code>, notifies the clients that the resource has been removed.
*
* @author Cristi
* @author Mariana
*
*/
public void unsubscribeAllClientsForcefully(String editableResourcePath, boolean displayMessageToClient) {
logger.debug("Unsubscribing all clients forcefully from Editable Resource = {}", editableResourcePath);
namedLockPool.lock(editableResourcePath);
try {
EditableResource er = editableResources.get(editableResourcePath);
if (er == null) {
logger.warn("Editable Resource not found for path = {}", editableResourcePath);
return;
}
// we use a copy to avoid ConcurrentModification...; it would have been too complicated to pass
// a runnable to delete using this iterator, and given the fact that this operation is not very
// frequent, we chose to copy
List<EditableResourceClient> existingClients = new ArrayList<EditableResourceClient>(er.getClients().size());
existingClients.addAll(er.getClients());
for (EditableResourceClient client : existingClients) {
if (displayMessageToClient) {
client.getCommunicationChannel().appendOrSendCommand(
new DisplaySimpleMessageClientCommand(
"Info",
"Resource " + er.getLabel() + " has been removed, thus you have been unsubscribed from it.",
DisplaySimpleMessageClientCommand.ICON_INFORMATION));
}
unsubscribeClientForcefully(client, editableResourcePath);
}
} finally {
namedLockPool.unlock(editableResourcePath);
}
}
/**
*
*/
protected void doSubscribeForcefully(StatefulServiceInvocationContext context, EditorStatefulClientLocalState state, EditableResourceClient client) {
if (context.getStatefulClientId() != null) {
logger.warn("For Editable Resource = {} and client = {}, trying to subscribe forcefully, but the call seems to come from client", state.getEditableResourcePath(), client.getCommunicationChannel());
return;
}
client.getCommunicationChannel().appendOrSendCommand(new CreateEditorStatefulClientCommand(
state.getEditableResourcePath(), editorName, state.isHandleAsClientSubscription()));
// set the new generated id
String statefulClientId = calculateStatefulClientId(state.getEditableResourcePath());
client.setStatefulClientId(statefulClientId);
context.setStatefulClientId(statefulClientId);
}
/**
* This is NOT thread safe.
*
* <p>
* Unlocks the resource (if <code>client</code> is the lock owner).
*
* TODO CS/STFL cred ca dupa mutare runnable, putem pune inapoi protected
*
*/
public boolean unlock(EditableResource editableResource, EditableResourceClient client) {
boolean unlockedNow = false;
if (editableResource.isLocked() && (client == null || client.equals(editableResource.getLockOwner()))) {
editableResource.setLocked(false);
editableResource.setLockExpireTime(null);
unlockedNow = true;
if (logger.isTraceEnabled()) {
logger.trace("Unlocked Editable Resource = {}, client = {}", editableResource.getEditableResourcePath(), client != null ? client.getCommunicationChannel() : null);
}
// for resource in "editable" state we need to mention last user that modified the file and when
// so we don't reset lockOwner and lockUpdateTime
// we don't shutdown the scheduled task (ScheduledFuture) because we don't have a reference towards it; when
// the timer will trigger, the corresponding runnable won't do anything. If this is not enough, we might add
// a reference from ER -> ScheduledFuture, to be able to cancel it quicker
} else {
// This shouldn't normally happen
logger.error("Unlock failed for Editable Resource = {}, client = {}. The resource isLocked = {} by client = {}",
new Object[] { editableResource.getEditableResourcePath(), client != null ? client.getCommunicationChannel() : null, editableResource.isLocked(), editableResource.getLockOwner().getCommunicationChannel() } );
}
return unlockedNow;
}
// //TODO : Temporary code (see #6777)
// /**
// *
// */
// protected abstract File getEditableResourceFile(EditableResource editableResource);
/**
* @return the <code>friendlName</code> encoded.
* @see EditorStatefulService#getFriendlyEditableResourcePath()
*
* @author Cristina
*/
public String getFriendlyNameEncoded(String friendlyName) {
return friendlyName;
}
/**
* @return the <code>friendlName</code> decoded.
* @see EditorStatefulService#getCanonicalEditableResourcePath()
*
* @author Cristina
*/
public String getFriendlyNameDecoded(String friendlyName) {
return EditorPlugin.getInstance().getFriendlyNameDecoded(friendlyName);
}
/**
* Doesn't have a channel parameter because this method would always be called for a resource after loading it's master resource.
* <p>
* Doesn't have validationProblems parameter because this method is safely called with input created internally whereas the other method
* is called with input given by the user.
*
* @see #getCanonicalEditableResourcePath(String, CommunicationChannel, StringBuffer)
*
*
* @author Sorin
*
*/
public String getFriendlyEditableResourcePath(String canonicalEditableResourcePath) {
return getFriendlyNameEncoded(canonicalEditableResourcePath);
}
/**
* @param channel needed because certain editors may present resource from a master resource that first must be opened for the current user (like diagram from model)
* @param validationProblems problems resulted from validating the path. This must be rigorously validated because it is called with input directly from user
* @return the canonical editableResourcePath or null if there were validation problems
*
*
* @author Sorin
*
*/
public String getCanonicalEditableResourcePath(String friendlyEditableResourcePath, CommunicationChannel channel, StringBuffer validationProblems) {
return getFriendlyNameDecoded(friendlyEditableResourcePath);
}
/**
* Method called to navigate to the given <code>fragment</code> by the external url feature.
* Each implementation should interpret <code>fragment</code> in it's own way.
*
* @param editableResourcePath the resource that was already opened in an editor
* @param fragment a string used to located the desired fragment. It is recommended to have a key=value
* @param channel the client in which the editableResourcePath is opened in an editor.
*
*
* @author Sorin
*
*/
public void navigateToFragment(CommunicationChannel channel, String editableResourcePath, String fragment) {
}
///////////////////////////////////////////////////////////////
// @RemoteInvocation methods
///////////////////////////////////////////////////////////////
/**
* Called from the client <code>EditorFrontendController</code> when a new resource is opening (e.g. from tree
* or at startup).<strong>NOTE:</strong> If a client has multiple editors for the same editorInput, it has only one subscription
* on the server.
*
* <p>
* If the client is already subscribed to the {@link EditableResource} corresponding to <code>editorInput</code>,
* the {@link EditableResource} is returned.
*
* <p>
* Otherwise, if the {@link EditableResource} exists (i.e. other clients are viewing it), the new client will
* subscribe to it (and we'll send the full content of the resource), and all clients will get notifications.
*
* <p>
* Otherwise, if the {@link EditableResource} is not open it is loaded from disk and the flow from above
* is repeated.
*
* <p>
* If the {@link EditableResource} is slave, we subscribe the client to its master {@link EditableResource} as well.
*
* @author Cristi
* @author Mariana
*
*
*/
@Override
@RemoteInvocation
public void subscribe(StatefulServiceInvocationContext context, IStatefulClientLocalState statefulClientLocalState) {
EditorStatefulClientLocalState state = (EditorStatefulClientLocalState) statefulClientLocalState;
AuditDetails auditDetails = new AuditDetails(logger, SUBSCRIBE_ER_AUDIT_CATEGORY, getFriendlyEditableResourcePath(calculateStatefulClientId(state.getEditableResourcePath())));
// lock by the path of the resource; any other thread with same path will wait
namedLockPool.lock(state.getEditableResourcePath());
try {
// get the editable resource from the registry; if it doesn't exist, create the resource and register it
// Here is the instruction that, without the synchronization on name, would have caused problems: 2 threads
// see that there is no EditableResource for the given name => both create. Only one remains in map. This
// may lead to memory leaks and other possibly unhealthy behaviors.
//
// locking the whole list would solve the issue, but with big performance impact
EditableResource editableResource = editableResources.get(state.getEditableResourcePath());
if (editableResource != null) {
logger.debug("EditableResource found for path = {}; reusing it", state.getEditableResourcePath());
if (editableResource.getEditableResourceClientByCommunicationChannel(context.getCommunicationChannel()) != null) {
// client already subscribed
// this scenario happens for tree nodes; each model that's opened => this method is called, but the client might be already subscribed
logger.trace("For Editable Resource = {}, client = {} is already subscribed.", state.getEditableResourcePath(), context.getCommunicationChannel());
return;
}
} else {
logger.debug("EditableResource not found for path = {}; creating and registering a new one", state.getEditableResourcePath());
editableResource = createEditableResourceInstance();
editableResource.setEditorStatefulService(this);
editableResource.setEditorInput(state.getEditableResourcePath());
if (!loadEditableResourceSafe(context, editableResource)) {
// i.e. load has thrown an error
return;
}
editableResource.updateLastModifiedStamp();
editableResources.put(state.getEditableResourcePath(), editableResource);
}
if (logger.isTraceEnabled()) {
logger.trace("For Editable Resource = {} adding client = {}. There were {} clients subscribed to this resource.", new Object[] { state.getEditableResourcePath(), context.getCommunicationChannel(), editableResource.getClients().size() });
}
EditableResourceClient client = new EditableResourceClient(context.getCommunicationChannel());
client.setStatefulClientId(context.getStatefulClientId());
// if this resource is a slave, subscribe to its master
if (editableResource.getMasterEditableResource() != null) {
editableResource.getMasterEditableResource().getEditorStatefulService().subscribeClientForcefully(client.getCommunicationChannel(), editableResource.getMasterEditableResource().getEditableResourcePath());
}
if (state.isForcingSubscriptionFromServer()) {
// this method will populate context & client with statefulClientId
doSubscribeForcefully(context, state, client);
}
editableResource.addEditableResourceClient(client);
doOnClientSubscribed(editableResource, client);
dispatchClientSubscribedToEditableResource(editableResource, client);
if (editableResource.getCollaborativeFigureModels() != null) {
invokeClientMethod(context.getCommunicationChannel(), context.getStatefulClientId(), "updateCollaborativeFigureModels", new Object[] { editableResource.getCollaborativeFigureModels(), true });
}
// TODO CS/FP2 dezact teste permisii
// //TODO : Temporary code (see #6777)
// // send notification to client in order to disable editing for client with no write permissions
// if (!AbstractSecurityUtils.INSTANCE.hasWritePermission(getEditableResourceFile(editableResource))) {
// invokeClientMethod(context.getCommunicationChannel(), context.getStatefulClientId(), "disableEditing", null);
// }
// TODO CS/FP2 dezact recent act
// SingletonRefsInEditorPluginFromWebPlugin.INSTANCE_ACTIVITY_LISTENER.updateLastAccess(editableResource);
} finally {
namedLockPool.unlock(state.getEditableResourcePath());
LogUtil.audit(auditDetails);
}
}
/**
* Used by {@link #subscribe()} and {@link #reloadEditableResource()}.
*
* <p>
* Invokes <code>updateEditableResourceStatus()</code> and sends full content.
*
*/
protected void doOnClientSubscribed(EditableResource editableResource, EditableResourceClient client) {
invokeClientMethod(client.getCommunicationChannel(), client.getStatefulClientId(), "updateEditableResourceStatus", new Object[] { editableResource });
// send the data from this resource to the client
sendFullContentToClient(editableResource, client);
}
/**
*
*/
@RemoteInvocation
public void unsubscribe(StatefulServiceInvocationContext context, IStatefulClientLocalState statefulClientLocalState) {
unsubscribeInternal(
context.getCommunicationChannel(),
((EditorStatefulClientLocalState) statefulClientLocalState).getEditableResourcePath(),
standardRemoveEditableResourceRunnable, false);
}
/**
* Called by the client when the last editor closes. Unsubscribes the current client from the
* {@link EditableResource}. If no other clients use the resource, then it is disposed.
*
* <p>
* Notifications are sent to all clients: the current client is told to remove the {@link EditableResource}
* from the system, and the other clients are informed that a client has left.
*
* <p>
* If this client has the lock on the resource, the resource is unlocked.
*
* <p>
* If subclasses need to add behavior here, they should use {@link #doUnsubscribeFromEditableResourceAdditionalLogic()}.
*
*
* @param saveResourceBeforeDisposing TODO
*/
protected void unsubscribeInternal(CommunicationChannel communicationChannel, String editableResourcePath, RunnableWithParam<Void, EditableResource> removeEditableResourceRunnable, boolean saveResourceBeforeDisposing) {
AuditDetails auditDetails = new AuditDetails(logger, "UNSUBSCRIBE_ER", getFriendlyEditableResourcePath(calculateStatefulClientId(editableResourcePath)));
namedLockPool.lock(editableResourcePath);
try {
EditableResource editableResource = editableResources.get(editableResourcePath);
if (editableResource == null) {
logger.error("The Editable Resource with path = {} was not found", editableResourcePath);
return;
}
// remove the client
EditableResourceClient client = editableResource.removeEditableResourceClientByCommunicationChannel(communicationChannel);
if (client == null) {
logger.error("For the Editable Resource = {}, the client = {} is not subscribed", editableResourcePath, communicationChannel);
return;
}
if (editableResource.isLocked() && editableResource.getLockOwner().equals(client)) {
// if this client has lock => unlock
unlock(editableResource, client);
dispatchEditableResourceStatus(editableResource);
}
if (logger.isTraceEnabled())
logger.trace("For the Editable Resource = {}, removing client = {}. Now there are {} clients subscribed to this resource.", new Object[] { editableResourcePath, communicationChannel, editableResource.getClients().size() });
if (editableResource.getSlaveEditableResources() != null) {
// force unsubscription for slave ERs
// we do a copy to avoid ConcurrentModificationException. It would be complicated to pass the current iterator
// and the performance penalty is not that big
List<EditableResource> slaveEditableResources = new ArrayList<EditableResource>(editableResource.getSlaveEditableResources());
for (EditableResource slaveER : slaveEditableResources) {
EditableResourceClient slaveClient = slaveER.getEditableResourceClientByCommunicationChannelThreadSafe(communicationChannel);
if (slaveClient != null) {
// slave client may be null; e.g. client 1 = model, model/diagr1; client 2 = model, model/diagr2. Unsubscribing
// client 1 from model, will iterate on diagr1, diagr2; but diagr2 is not associated with client 1, i.e. slaveClient == null
slaveER.getEditorStatefulService().unsubscribeClientForcefully(slaveClient, slaveER.getEditableResourcePath());
}
}
}
doOnClientUnsubscribed(editableResource, client);
dispatchClientUnsubscribedFromEditableResource(editableResource, client);
if (editableResource.getClients().size() == 0) {
if (editableResource.isDirty() && editableResource.getMasterEditableResource() == null && saveResourceBeforeDisposing) {
logger.debug("Saving ER = {} automatically before dispose.", editableResource.getEditableResourcePath());
// the save method needs only the cc; the statefulClientId is not needed; so we can generate a fake SSIC here,
// to avoid to write another method that takes only CC, etc.
save(new StatefulServiceInvocationContext(communicationChannel), editableResource.getEditableResourcePath());
}
disposeEditableResourceSafe(editableResource);
removeEditableResourceRunnable.run(editableResource);
}
} finally {
namedLockPool.unlock(editableResourcePath);
LogUtil.audit(auditDetails);
}
}
/**
* Used by {@link #unsubscribeInternal()} and {@link #reloadEditableResource()}.
*
* <p>
* May be overridden if logic needs to be executed when a client disconnects from an
* {@link EditableResource}.
*
*
*/
protected void doOnClientUnsubscribed(EditableResource editableResource, EditableResourceClient client) {
// do nothing by default
}
/**
* Called by the client when it has updates.
*
* <p>
* If the {@link EditableResource} is locked, then the attempt is unsuccessful, and if
* {@link #areLocalUpdatesAppliedImmediately()} then a full content is sent again to
* the server.
*
* <p>
* Otherwise (i.e. not locked), then the resource is locked, the content update custom
* server logic is invoked (i.e. {@link #updateEditableResourceContentAndDispatchUpdates()}) and if
* the dirty state or lock has changed, the client is notified (using {@link UpdateEditableResourceClientCommand}).
*
* <p>
* If the {@link EditableResource} is slave, and the dirty state changes, the notification is
* dispatched for the master {@link EditableResource}, not for the slave.
*
* @return <code>true</code> if the updates were accepted. <code>false</code> otherwise.
*/
@RemoteInvocation
public boolean attemptUpdateEditableResourceContent(StatefulServiceInvocationContext context, String editableResourcePath, Object updatesToApply) {
boolean lockAquired = false;
logger.trace("Attempting to update Editable Resource = {}, client = {}", editableResourcePath, context.getCommunicationChannel());
namedLockPool.lock(editableResourcePath);
try {
EditableResource editableResource = editableResources.get(editableResourcePath);
if (editableResource == null) {
return false;
}
// TODO CS/FP2 dezact permission check
// //TODO : Temporary code (see #6777)
// if (!AbstractSecurityUtils.INSTANCE.hasWritePermission(getEditableResourceFile(editableResource))) {
// logger.error("The user cannot update content because he doesn't have write permissions!");
// return;
// }
EditableResourceClient client = editableResource.getEditableResourceClientByCommunicationChannel(context.getCommunicationChannel());
boolean initialDirtyState = editableResource.isDirty();
boolean initialLockState = editableResource.isLocked();
lockAquired = tryLock(editableResource, client);
if (lockAquired) {
// lock acquired or renewed
updateEditableResourceContentAndDispatchUpdates(context, editableResource, updatesToApply);
// dispatch ER notification(s)
if (editableResource.getMasterEditableResource() == null) {
// normal Editable Resource
if (initialDirtyState != editableResource.isDirty() || initialLockState != editableResource.isLocked()) {
dispatchEditableResourceStatus(editableResource);
}
} else {
// slave ER
if (initialDirtyState != editableResource.isDirty()) {
// dispatch dirty notification for the master ER
editableResource.getMasterEditableResource().getEditorStatefulService().dispatchEditableResourceStatus(editableResource.getMasterEditableResource());
}
if (initialLockState != editableResource.isLocked()) {
// dispatch lock notification for the slave ER
dispatchEditableResourceStatus(editableResource);
}
}
} else {
// lock failed; someone else was quicker
if (areLocalUpdatesAppliedImmediately()) {
// reinitialize only in this case; otherwise, the client has still the content valid
// because it waits for an update from the server
sendFullContentToClient(editableResource, client);
}
}
} finally {
namedLockPool.unlock(editableResourcePath);
}
return lockAquired;
}
// protected boolean lock(EditableResource editableResource, EditableResourceClient client) {
// return EditorSupport.INSTANCE.lock(editableResource, client);
// }
/**
*
*/
@RemoteInvocation
public boolean tryLockFromButton(StatefulServiceInvocationContext context, String editableResourcePath) {
namedLockPool.lock(editableResourcePath);
try {
EditableResource editableResource = editableResources.get(editableResourcePath);
if (editableResource == null) {
logger.error("The Editable Resource with path = {} was not found", editableResourcePath);
}
EditableResourceClient client = editableResource.getEditableResourceClientByCommunicationChannel(context.getCommunicationChannel());
if (client == null) {
logger.error("For the Editable Resource = {}, the client = {} is not subscribed", editableResourcePath, context.getCommunicationChannel());
}
if (tryLock(editableResource, client)) {
dispatchEditableResourceStatus(editableResource);
return true;
} else {
return false;
}
} finally {
namedLockPool.unlock(editableResourcePath);
}
}
/**
*
*/
@RemoteInvocation
public boolean unlockFromButton(StatefulServiceInvocationContext context, String editableResourcePath) {
namedLockPool.lock(editableResourcePath);
try {
EditableResource editableResource = editableResources.get(editableResourcePath);
if (editableResource == null) {
logger.error("The Editable Resource with path = {} was not found", editableResourcePath);
}
EditableResourceClient client = editableResource.getEditableResourceClientByCommunicationChannel(context.getCommunicationChannel());
if (client == null) {
logger.error("For the Editable Resource = {}, the client = {} is not subscribed", editableResourcePath, context.getCommunicationChannel());
}
if (unlock(editableResource, client)) {
dispatchEditableResourceStatus(editableResource);
return true;
} else {
return false;
}
} finally {
namedLockPool.unlock(editableResourcePath);
}
}
/**
*
*/
@RemoteInvocation
public long addCollaborativeFigureModel(StatefulServiceInvocationContext context, String editableResourcePath, CollaborativeFigureModel model) {
logger.trace("For Editable Resource = {}, client = {}, adding CollaborativeFigureModel", editableResourcePath, context.getCommunicationChannel());
namedLockPool.lock(editableResourcePath);
try {
model.setId(collaborativeFigureModelsIdFactory.getAndIncrement());
updateCollaborativeFigureModels(context, editableResourcePath, Collections.singleton(model));
return model.getId();
} finally {
namedLockPool.unlock(editableResourcePath);
}
}
/**
*
*/
@RemoteInvocation
public void updateCollaborativeFigureModels(StatefulServiceInvocationContext context, String editableResourcePath, Collection<CollaborativeFigureModel> models) {
logger.trace("For Editable Resource = {}, client = {}, updating CollaborativeFigureModel and dispatching updates", editableResourcePath, context.getCommunicationChannel());
namedLockPool.lock(editableResourcePath);
try {
EditableResource editableResource = editableResources.get(editableResourcePath);
if (editableResource == null) {
logger.error("Editable Resource not found for path = {}", editableResourcePath);
return;
}
if (editableResource.getCollaborativeFigureModels() == null) {
// lazy init the collection
editableResource.setCollaborativeFigureModels(new HashSet<CollaborativeFigureModel>());
}
for (CollaborativeFigureModel model : models) {
// being a set, add a new instance with same id, will overwrite the existing instance
editableResource.getCollaborativeFigureModels().add(model);
}
for (EditableResourceClient otherClient : editableResource.getClients()) {
if (!context.getCommunicationChannel().equals(otherClient.getCommunicationChannel())) {
// don't send to the initiator, because it has already applied the updates
invokeClientMethod(otherClient.getCommunicationChannel(), otherClient.getStatefulClientId(), "updateCollaborativeFigureModels", new Object[] { models, false });
}
}
} finally {
namedLockPool.unlock(editableResourcePath);
}
}
/**
*
*/
@RemoteInvocation
private void removeCollaborationFigureModelWithId(Collection<CollaborativeFigureModel> models, int id) {
for (Iterator<CollaborativeFigureModel> iter = models.iterator(); iter.hasNext(); ) {
CollaborativeFigureModel model = iter.next();
if (model.getId() == id) {
iter.remove();
break;
}
}
}
/**
*
*/
@RemoteInvocation
public void removeCollaborativeFigureModels(StatefulServiceInvocationContext context, String editableResourcePath, Collection<Integer> modelIds) {
logger.trace("For Editable Resource = {}, client = {}, removing CollaborativeFigureModel and dispatching updates", editableResourcePath, context.getCommunicationChannel());
namedLockPool.lock(editableResourcePath);
try {
EditableResource editableResource = editableResources.get(editableResourcePath);
if (editableResource == null) {
logger.error("Editable Resource not found for path = {}", editableResourcePath);
return;
}
if (editableResource.getCollaborativeFigureModels() == null) {
logger.error("Trying to remove some CollaborativeFigureModels, but the list is null");
return;
}
for (int id : modelIds) {
removeCollaborationFigureModelWithId(editableResource.getCollaborativeFigureModels(), id);
}
if (editableResource.getCollaborativeFigureModels().size() == 0) {
editableResource.setCollaborativeFigureModels(null);
}
for (EditableResourceClient otherClient : editableResource.getClients()) {
if (!context.getCommunicationChannel().equals(otherClient.getCommunicationChannel())) {
// don't send to the initiator, because it has already applied the updates
invokeClientMethod(otherClient.getCommunicationChannel(), otherClient.getStatefulClientId(), "removeCollaborativeFigureModels", new Object[] { modelIds });
}
}
} finally {
namedLockPool.unlock(editableResourcePath);
}
}
/**
*
*/
public void revealEditor(CommunicationChannel channel, String editableResourcePath) {
EditableResource editableResource = getEditableResource(editableResourcePath);
if (editableResource == null)
return; // Resource wasn't opened at all
EditableResourceClient editableResourceClient = editableResource.getEditableResourceClientByCommunicationChannel(channel);
if (editableResourceClient == null)
return; // The client didn't manage to open the resource
invokeClientMethod(channel, editableResourceClient.getStatefulClientId(), "revealEditor", new Object[] {});
}
/**
* @see ActivityService#getIconUrl(String)
*
* @author Mariana
*/
public String getIconUrl() {
return createEditableResourceInstance().getIconUrl();
}
}