/* 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.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import org.flowerplatform.communication.channel.CommunicationChannel;
import org.flowerplatform.editor.EditorPlugin;
import org.flowerplatform.editor.collaboration.CollaborativeFigureModel;
/**
* The corresponding ActionScript class has a special mapping. Some fields exist only
* in Java, or only in AS, or both in AS and Java but they are not serialized, or both
* in AS and Java and they are serialized. The comment of each getter specifies this.
*
* <p>
* Represents a resource that can be edited on the client side (in an editor or not).
* E.g. a text file, a model file, a diagram.
*
* <p>
* It contains fields related to the state of the resource (i.e. fields existent
* in this class) and fields related to the content of the resource (which are provided
* by the subclass, according to the data it holds).
*
* <p>
* On the server side, an {@link EditableResource} means state + content, and there
* is a specialized class for each type of {@link EditableResource}. On the client side,
* the {@link EditableResource} holds only the state (and there are no {@link EditableResource}
* subclasses). The content is held by <code>EditorFrontend</code>s.
*
* @author Cristi
*
*/
public abstract class EditableResource {
private EditorStatefulService editorStatefulService;
/**
* This property is NOT transferred to the client.
*/
private Object file;
/**
* @see Getter
*
*/
private Object editorInput;
/**
* This is not synchronized on purpose.
*
* @see Getter
*
*/
private List<EditableResourceClient> clients = new ArrayList<EditableResourceClient>();
private List<EditableResourceClient> clientsReadOnly = Collections.unmodifiableList(clients);
/**
* @see Getter.
*
*/
private boolean locked;
/**
* @see Getter.
*
*/
private Date lockExpireTime;
/**
* @see Getter.
*
*/
private Date lockUpdateTime;
/**
* @see Getter.
*
*/
private long lastModifiedTimestamp;
private EditableResourceClient lockOwner;
private Collection<CollaborativeFigureModel> collaborativeFigureModels;
public Object getFile() {
return file;
}
public void setFile(Object file) {
this.file = file;
}
public long getLastModifiedStamp() {
return lastModifiedTimestamp;
}
public void updateLastModifiedStamp() {
if (getFile() != null) {
this.lastModifiedTimestamp = EditorPlugin.getInstance().getFileAccessController().getLastModifiedTimestamp(getFile());
}
}
public boolean isSynchronized() {
if (getFile() != null) {
return lastModifiedTimestamp == EditorPlugin.getInstance().getFileAccessController().getLastModifiedTimestamp(getFile());
}
return true;
}
public EditorStatefulService getEditorStatefulService() {
return editorStatefulService;
}
public void setEditorStatefulService(EditorStatefulService editorStatefulService) {
this.editorStatefulService = editorStatefulService;
}
public String getEditorStatefulClientId() {
return getEditorStatefulService().calculateStatefulClientId(getEditableResourcePath());
}
public String getMasterEditorStatefulClientId() {
if (getMasterEditableResource() == null) {
return null;
} else {
return getMasterEditableResource().getEditorStatefulClientId();
}
}
/**
* This property is transferred to the client.
*
* The editorInput is an unique identifier of an {@link EditableResource},
* which is valid regardless of the state of the server and the {@link EditableResource}
* (i.e. it is not a transient id).
*
* <p>
* For the moment, the convention is to use a string that represents the path
* of the resource (if it is a file, e.g. /org/ws/proj.txt) or a the path of the
* resource + the internal path (e.g. for a model + diagram can be /org/ws/model.flower/!xmi_id_of_element).
*
* <p>
* The editor backend (server)/editor frontend (client) pair interpret (or unpack)
* the editorInput. They are the only ones allowed to cast the value to something meaningful
* (probably a <code>String</code>). Everyone else, should treat this as an opaque <code>Object</code>, and
* should not have logic related to "what's in" the editorInput". It can be of course used for
* comparison.
*
*
*/
public Object getEditorInput() {
return editorInput;
}
public String getEditableResourcePath() {
return editorInput.toString();
}
/**
* @see Getter
*
*/
public void setEditorInput(Object editorInput) {
this.editorInput = editorInput;
}
/**
* This property is NOT transferred to the client.
*
* By default returns <code>null</code> and should be overridden by classes
* that are slave {@link EditableResource}s (i.e. there is another {@link EditableResource}
* that represents the physical file (e.g. a model file) and they are are sub-units
* (e.g. a diagram) that cannot be separately saved).
*
* <p>
* In a master-slave relationship, a slave {@link EditableResource} cannot be saved
* separately. Saving makes sense only on maste {@link EditableResource}s.
*
* <p>
* The field is not declared in this class to save some memory (there are a lot of
* {@link EditableResource}s that are not master/slave and they don't need this field).
*
*
*/
public EditableResource getMasterEditableResource() {
return null;
}
/**
* This property is NOT transferred to the client, although it exists there, maintained
* by client logic.
*
* <p>
* By default returns <code>null</code> and should be overridden by classes
* that are master {@link EditableResource}s.
*
* <p>
* The field is not declared in this class to save some memory (there are a lot of
* {@link EditableResource}s that are not master/slave and they don't need this field).
*/
public List<EditableResource> getSlaveEditableResources() {
return null;
}
/**
* By default does nothing and should be overridden by classes
* that are master {@link EditableResource}s. The method should set
* the underlying field. This method exists, because this list
* is lazy initialized, to save some memory.
*/
public void setSlaveEditableResources(List<EditableResource> value) {
// do nothing
}
/**
* This property is transferred to the client.
*
* Should return the label (displayed in the Open Resources View
* and when the Save dialog pops up).
*
*
*/
public abstract String getLabel();
/**
* This property is transferred to the client.
*
* Should return the URL for the icon (displayed in the Open Resources View
* and when the Save dialog pops up).
*
*
*/
public abstract String getIconUrl();
/**
* This property is transferred to the client.
*
* Should return the dirty state of the {@link EditableResource}
* (i.e. <code>true</code> if it needs to be saved and <code>false</code>
* if it doesn't have unsaved changes).
*
*
*/
public abstract boolean isDirty();
/**
* The lock status of the current resource.
*
*
*/
public boolean isLocked() {
return locked;
}
/**
*
*/
public void setLocked(boolean locked) {
this.locked = locked;
}
/**
* The time when the lock will expire.
*
*
*/
public Date getLockExpireTime() {
return lockExpireTime;
}
/**
*
*/
public void setLockExpireTime(Date lockExpireTime) {
this.lockExpireTime = lockExpireTime;
}
/**
* The time when the lock info has been updated.
*
*
*/
public Date getLockUpdateTime() {
return lockUpdateTime;
}
/**
*
*/
public void setLockUpdateTime(Date lockUpdateTime) {
this.lockUpdateTime = lockUpdateTime;
}
/**
* The owner of the lock.
*
*
*/
public EditableResourceClient getLockOwner() {
return lockOwner;
}
/**
*
*/
public void setLockOwner(EditableResourceClient lockOwner) {
this.lockOwner = lockOwner;
}
/**
* This property exists on the client, but is not transferred via serialization
* (i.e. is Transient on the client).
*
* <p>
* Contains the clients that are currently viewing this {@link EditableResource}. The list
* is read-only. This class has methods for adding and removing elements in it.
*
* <p>
* Please make sure that the code that uses methods related to this list (including this one,
* used for iteration), uses a named lock, from {@link EditorStatefulService#namedLockPool}.
* Unless explicitly specified otherwise in the method's doc.
*
* TODO CS/STFL adaugat @see cu metodele respective, cand le definitivam
* TODO CS/STFL de verificat ca toate apelurile sunt cu lock catre ac. metode
*/
public List<EditableResourceClient> getClients() {
return clientsReadOnly;
}
/**
* Iterates the clients and returns the one having the given
* {@link CommunicationChannel} (or <code>null</code> if none found).
*
*
*/
public EditableResourceClient getEditableResourceClientByCommunicationChannel(
final CommunicationChannel communicationChannel) {
// synchronized (clients) {
for (EditableResourceClient client : clients) {
if (client.getCommunicationChannel().equals(communicationChannel)) {
return client;
}
}
return null;
// }
}
/**
* Used by {@link #removeEditableResourceClientByCommunicationChannel(CommunicationChannel)} and
* {@link #getEditableResourceClientByCommunicationChannelThreadSafe(CommunicationChannel)} to ensure that
* no removal can be done from the list while the iteration is in progress.
*
* @see #getEditableResourceClientByCommunicationChannelThreadSafe(CommunicationChannel)
* @see #removeEditableResourceClientByCommunicationChannel(CommunicationChannel)
*/
private ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
/**
* Iterates the clients and returns the one having the given
* {@link CommunicationChannel}, or <code>null</code> if not found.
*
* <p>
* This method is synchronized and shares a read/write lock only with {@link #removeEditableResourceClientByCommunicationChannel(CommunicationChannel)}.
* That means that while a iterating this list, a removal cannot take place. However, an
* addition can happen, and in this case the iteration won't take the newly added element into consideration.
*
* <p>
* This method is intended to be used within {@link EditorStatefulService#webCommunicationChannelDestroyed()}. For
* this usage, the fact that new items can be added to the list, is not disturbing. The fact that the addition is
* not synchronized improves the performance (at least in theory).
*
* <p>
* Because we are using a Read/Write lock, multiple threads can execute this method (having a Read lock) at the same time.
*/
public EditableResourceClient getEditableResourceClientByCommunicationChannelThreadSafe(
final CommunicationChannel communicationChannel) {
readWriteLock.readLock().lock();
try {
// we use iteration with for instead of for each, because new elements might
// be added to the list, and the iterator would complain. Cf. this method comment,
// this kind of concurrent read/write access is valid. However, we wouldn't have liked
// the opposite: to have elements removed. That's why we use the read/write lock
for (int i = 0; i < clients.size(); i++) {
EditableResourceClient client = clients.get(i);
if (client.getCommunicationChannel().equals(communicationChannel)) {
return client;
}
}
// no client found
return null;
} finally {
readWriteLock.readLock().unlock();
}
}
/**
* Iterates the clients and removes the one having the given
* {@link CommunicationChannel}. Returns it (or <code>null</code> if none found).
*
* <p>
* This method is synchronized and shares the read/write lock only with {@link #getEditableResourceClientByCommunicationChannelThreadSafe(CommunicationChannel)}.
* So a list removal cannot take place when the former method iterates on the list.
*
* @see #getEditableResourceClientByCommunicationChannelThreadSafe(CommunicationChannel)
*
*/
public EditableResourceClient removeEditableResourceClientByCommunicationChannel(
CommunicationChannel communicationChannel) {
readWriteLock.writeLock().lock();
try {
for (Iterator<EditableResourceClient> iter = clients.iterator(); iter.hasNext(); ) {
EditableResourceClient client = iter.next();
if (client.getCommunicationChannel().equals(communicationChannel)) {
// found
iter.remove();
return client;
}
}
return null;
} finally {
readWriteLock.writeLock().unlock();
}
}
public void addEditableResourceClient(EditableResourceClient client) {
clients.add(client);
}
@Override
public String toString() {
if (getSlaveEditableResources() != null) {
return String.format("%s[path=%s, slaveERs=%s]", getClass().getSimpleName(), getEditorInput(), getSlaveEditableResources());
} else {
return String.format("%s[path=%s]", getClass().getSimpleName(), getEditorInput());
}
}
public Collection<CollaborativeFigureModel> getCollaborativeFigureModels() {
return collaborativeFigureModels;
}
public void setCollaborativeFigureModels(
Collection<CollaborativeFigureModel> collaborativeFigureModels) {
this.collaborativeFigureModels = collaborativeFigureModels;
}
}