Edition lock is working. Working on user interation.

This commit is contained in:
Paulo Gustavo Veiga 2012-10-04 20:28:59 -03:00 committed by Paulo Gustavo Veiga
parent 04d43e62c7
commit 6303ba93c7
14 changed files with 350 additions and 90 deletions

View File

@ -18,14 +18,19 @@
mindplot.RESTPersistenceManager = new Class({
Extends:mindplot.PersistenceManager,
initialize:function (saveUrl, revertUrl, lockUrl) {
initialize:function (options) {
this.parent();
$assert(saveUrl, "saveUrl can not be null");
$assert(revertUrl, "revertUrl can not be null");
this.saveUrl = saveUrl;
this.revertUrl = revertUrl;
this.lockUrl = lockUrl;
this.timestamp = null;
$assert(options.saveUrl, "saveUrl can not be null");
$assert(options.revertUrl, "revertUrl can not be null");
$assert(options.lockUrl, "lockUrl can not be null");
$assert(options.session, "session can not be null");
$assert(options.timestamp, "timestamp can not be null");
this.saveUrl = options.saveUrl;
this.revertUrl = options.revertUrl;
this.lockUrl = options.lockUrl;
this.timestamp = options.timestamp;
this.session = options.session;
},
saveMapXml:function (mapId, mapXml, pref, saveHistory, events, sync) {
@ -39,6 +44,7 @@ mindplot.RESTPersistenceManager = new Class({
var persistence = this;
var query = "minor=" + !saveHistory;
query = query + (this.timestamp ? "&timestamp=" + this.timestamp : "");
query = query + (this.session ? "&session=" + this.session : "");
var request = new Request({
url:this.saveUrl.replace("{id}", mapId) + "?" + query,

View File

@ -0,0 +1,83 @@
/*
* Copyright [2011] [wisemapping]
*
* Licensed under WiseMapping Public License, Version 1.0 (the "License").
* It is basically the Apache License, Version 2.0 (the "License") plus the
* "powered by wisemapping" text requirement on every single page;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the license at
*
* http://www.wisemapping.org/license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
mindplot.widget.ToolbarNotifier = new Class({
initialize:function () {
var container = $('headerNotifier');
// In case of print,embedded no message is displayed ....
if (container) {
this._effect = new Fx.Elements(container, {
onComplete:function () {
container.setStyle('display', 'none');
}.bind(this),
link:'cancel',
duration:8000,
transition:Fx.Transitions.Expo.easeInOut
});
}
},
logError:function (userMsg) {
this.logMessage(userMsg, mindplot.widget.ToolbarNotifier.MsgKind.ERROR);
},
hide:function () {
},
logMessage:function (msg, fade) {
$assert(msg, 'msg can not be null');
var container = $('headerNotifier');
// In case of print,embedded no message is displayed ....
if (container) {
container.set('text', msg);
container.setStyle('display', 'block');
container.position({
relativeTo:$('header'),
position:'upperCenter',
edge:'centerTop'
});
if (!$defined(fade) || fade) {
this._effect.start({
0:{
opacity:[1, 0]
}
});
} else {
container.setStyle('opacity', '1');
this._effect.pause();
}
}
}
});
mindplot.widget.ToolbarNotifier.MsgKind = {
INFO:1,
WARNING:2,
ERROR:3,
FATAL:4
};
var toolbarNotifier = new mindplot.widget.ToolbarNotifier();
$notify = toolbarNotifier.logMessage.bind(toolbarNotifier);

View File

@ -0,0 +1,38 @@
/*
* Copyright [2011] [wisemapping]
*
* Licensed under WiseMapping Public License, Version 1.0 (the "License").
* It is basically the Apache License, Version 2.0 (the "License") plus the
* "powered by wisemapping" text requirement on every single page;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the license at
*
* http://www.wisemapping.org/license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wisemapping.exceptions;
import org.jetbrains.annotations.NotNull;
public class EditionSessionExpiredException
extends ClientException
{
public static final String MSG_KEY = "MINDMAP_TIMESTAMP_OUTDATED";
public EditionSessionExpiredException(@NotNull String msg)
{
super(msg);
}
@NotNull
@Override
protected String getMsgBundleKey() {
return MSG_KEY;
}
}

View File

@ -20,12 +20,12 @@ package com.wisemapping.exceptions;
import org.jetbrains.annotations.NotNull;
public class MindmapOutdatedException
public class MultipleSessionsOpenException
extends ClientException
{
public static final String MSG_KEY = "MINDMAP_TIMESTAMP_OUTDATED";
public MindmapOutdatedException(@NotNull String msg)
public MultipleSessionsOpenException(@NotNull String msg)
{
super(msg);
}

View File

@ -0,0 +1,39 @@
/*
* Copyright [2011] [wisemapping]
*
* Licensed under WiseMapping Public License, Version 1.0 (the "License").
* It is basically the Apache License, Version 2.0 (the "License") plus the
* "powered by wisemapping" text requirement on every single page;
* you may not use this file except in compliance with the License.
* You may obtain a copy of the license at
*
* http://www.wisemapping.org/license
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.wisemapping.exceptions;
import com.wisemapping.model.Collaborator;
import org.jetbrains.annotations.NotNull;
public class SessionExpiredException
extends ClientException
{
public static final String MSG_KEY = "MINDMAP_TIMESTAMP_OUTDATED";
public SessionExpiredException(@NotNull String msg,@NotNull Collaborator newEditor)
{
super(msg);
}
@NotNull
@Override
protected String getMsgBundleKey() {
return MSG_KEY;
}
}

View File

@ -27,6 +27,7 @@ import com.wisemapping.model.Mindmap;
import com.wisemapping.model.MindMapHistory;
import com.wisemapping.model.User;
import com.wisemapping.security.Utils;
import com.wisemapping.service.LockInfo;
import com.wisemapping.service.LockManager;
import com.wisemapping.service.MindmapService;
import com.wisemapping.view.MindMapBean;
@ -147,7 +148,7 @@ public class MindmapController {
return showEditorPage(id, model, true);
}
private String showEditorPage(int id, @NotNull final Model model, boolean requiresLock) throws AccessDeniedSecurityException, LockException {
private String showEditorPage(int id, @NotNull final Model model, boolean requiresLock) throws WiseMappingException {
final MindMapBean mindmapBean = findMindmapBean(id);
final Mindmap mindmap = mindmapBean.getDelegated();
final User collaborator = Utils.getUser();
@ -159,10 +160,13 @@ public class MindmapController {
final LockManager lockManager = this.mindmapService.getLockManager();
if (lockManager.isLocked(mindmap) && !lockManager.isLockedBy(mindmap, collaborator)) {
readOnlyMode = true;
model.addAttribute("lockedBy", lockManager.getLockInfo(mindmap));
} else {
lockManager.lock(mindmap, collaborator);
final long session = lockManager.generateSession();
final LockInfo lock = lockManager.lock(mindmap, collaborator, session);
model.addAttribute("lockTimestamp", lock.getTimestamp());
model.addAttribute("lockSession", session);
}
model.addAttribute("lockInfo", lockManager.getLockInfo(mindmap));
}
// Set render attributes ...
@ -176,12 +180,12 @@ public class MindmapController {
}
@RequestMapping(value = "maps/{id}/view", method = RequestMethod.GET)
public String showMindmapViewerPage(@PathVariable int id, @NotNull Model model) throws LockException, AccessDeniedSecurityException {
public String showMindmapViewerPage(@PathVariable int id, @NotNull Model model) throws WiseMappingException {
return showEditorPage(id, model, false);
}
@RequestMapping(value = "maps/{id}/try", method = RequestMethod.GET)
public String showMindmapTryPage(@PathVariable int id, @NotNull Model model) throws LockException, AccessDeniedSecurityException {
public String showMindmapTryPage(@PathVariable int id, @NotNull Model model) throws WiseMappingException {
final String result = showEditorPage(id, model, false);
model.addAttribute("memoryPersistence", true);
return result;

View File

@ -19,9 +19,7 @@
package com.wisemapping.rest;
import com.wisemapping.exceptions.ImportUnexpectedException;
import com.wisemapping.exceptions.MindmapOutdatedException;
import com.wisemapping.exceptions.WiseMappingException;
import com.wisemapping.exceptions.*;
import com.wisemapping.importer.ImportFormat;
import com.wisemapping.importer.Importer;
import com.wisemapping.importer.ImporterException;
@ -30,10 +28,10 @@ import com.wisemapping.model.*;
import com.wisemapping.rest.model.*;
import com.wisemapping.security.Utils;
import com.wisemapping.service.CollaborationException;
import com.wisemapping.service.LockInfo;
import com.wisemapping.service.LockManager;
import com.wisemapping.service.MindmapService;
import com.wisemapping.validator.MapInfoValidator;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
@ -141,7 +139,7 @@ public class MindmapController extends BaseController {
@RequestMapping(method = RequestMethod.PUT, value = "/maps/{id}/document", consumes = {"application/xml", "application/json"}, produces = {"application/json", "text/html", "application/xml"})
@ResponseBody
public long updateDocument(@RequestBody RestMindmap restMindmap, @PathVariable int id, @RequestParam(required = false) boolean minor, @RequestParam(required = false) Long timestamp) throws WiseMappingException, IOException {
public long updateDocument(@RequestBody RestMindmap restMindmap, @PathVariable int id, @RequestParam(required = false) boolean minor, @RequestParam(required = false) Long timestamp, @RequestParam(required = false) Long session) throws WiseMappingException, IOException {
final Mindmap mindmap = mindmapService.findMindmapById(id);
final User user = Utils.getUser();
@ -152,10 +150,8 @@ public class MindmapController extends BaseController {
throw new IllegalArgumentException("Map properties can not be null");
}
// Check that there we are not overwriting an already existing map ...
if (timestamp != null && mindmap.getLastModificationTime().getTimeInMillis() > timestamp) {
throw new MindmapOutdatedException("Mindmap timestamp out of sync. Client timestamp: " + timestamp + ", DB Timestamp:" + timestamp);
}
// Could the map be updated ?
checkUpdate(mindmap, user, session, timestamp);
// Update collaboration properties ...
final CollaborationProperties collaborationProperties = mindmap.findCollaborationProperties(user);
@ -172,8 +168,36 @@ public class MindmapController extends BaseController {
logger.debug("Mindmap save completed:" + restMindmap.getXml());
saveMindmap(minor, mindmap, user);
// Return last update timestamp ...
return mindmap.getLastModificationTime().getTimeInMillis();
// Update edition timeout ...
final LockManager lockManager = mindmapService.getLockManager();
final LockInfo lockInfo = lockManager.updateExpirationTimeout(mindmap, user, session);
return lockInfo.getTimestamp();
}
private void checkUpdate(@NotNull Mindmap mindmap, @NotNull User user, long session, long timestamp) throws WiseMappingException {
// The lock was lost, reclaim as the ownership of it.
final LockManager lockManager = mindmapService.getLockManager();
final boolean lockLost = lockManager.isLocked(mindmap);
if (!lockLost) {
lockManager.lock(mindmap, user, session);
}
final LockInfo lockInfo = lockManager.getLockInfo(mindmap);
if (lockInfo.getCollaborator().equals(user)) {
final boolean outdated = mindmap.getLastModificationTime().getTimeInMillis() > timestamp;
if (lockInfo.getSession() == session) {
// Timestamp might not be returned to the client. This try to cover this case, ignoring the client timestamp check.
final User lastEditor = mindmap.getLastEditor();
if (outdated && (lockInfo.getPreviousTimestamp() != timestamp || lastEditor == null || !lastEditor.equals(user))) {
throw new MultipleSessionsOpenException("The map has been updated and not by you. Session lost.");
}
} else if (outdated) {
throw new MultipleSessionsOpenException("The map has been updated and not by you. Session lost.");
}
} else {
throw new SessionExpiredException("You have lost the edition session", lockInfo.getCollaborator());
}
}
/**
@ -361,7 +385,13 @@ public class MindmapController extends BaseController {
final User user = Utils.getUser();
final LockManager lockManager = mindmapService.getLockManager();
final Mindmap mindmap = mindmapService.findMindmapById(id);
lockManager.updateLock(Boolean.parseBoolean(value), mindmap, user);
final boolean lock = Boolean.parseBoolean(value);
if (!lock) {
lockManager.unlock(mindmap, user);
} else {
throw new UnsupportedOperationException("REST lock must be implemented.");
}
}
@RequestMapping(method = RequestMethod.DELETE, value = "/maps/batch")

View File

@ -24,14 +24,23 @@ import java.util.Set;
getterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY,
isGetterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY)
@JsonIgnoreProperties(ignoreUnknown = true)
public class RestMindmapLock {
public class RestLockInfo {
@NotNull
private Collaborator user;
@Nullable
private LockInfo lockInfo;
final private Collaborator user;
public RestMindmapLock(@Nullable LockInfo lockInfo, @NotNull Collaborator collaborator) {
@Nullable
final private LockInfo lockInfo;
// This is required only for compliance with the JAXB serializer.
public RestLockInfo(){
this.lockInfo = null;
//noinspection ConstantConditions
this.user = null;
}
public RestLockInfo(@Nullable LockInfo lockInfo, @NotNull Collaborator collaborator) {
this.lockInfo = lockInfo;
this.user = collaborator;
@ -52,4 +61,13 @@ public class RestMindmapLock {
public void setLockedByMe(boolean lockedForMe) {
// Ignore ...
}
public long getTimestamp() {
return lockInfo != null ? lockInfo.getTimestamp() : -1;
}
public void setTimestamp(long value) {
//
}
}

View File

@ -19,6 +19,7 @@
package com.wisemapping.service;
import com.wisemapping.model.Collaborator;
import com.wisemapping.model.Mindmap;
import org.jetbrains.annotations.NotNull;
import java.util.Calendar;
@ -26,11 +27,15 @@ import java.util.Calendar;
public class LockInfo {
final private Collaborator collaborator;
private Calendar timeout;
private static int EXPIRATION_MIN = 25;
private long session;
private static int EXPIRATION_MIN = 30;
private long timestamp = -1;
private long previousTimestamp;
public LockInfo(@NotNull Collaborator collaborator) {
public LockInfo(@NotNull Collaborator collaborator, @NotNull Mindmap mindmap, long session) {
this.collaborator = collaborator;
this.updateTimeout();
this.updateTimestamp(mindmap);
}
public Collaborator getCollaborator() {
@ -38,7 +43,7 @@ public class LockInfo {
}
public boolean isExpired() {
return timeout.before(Calendar.getInstance());
return timeout.before(Calendar.getInstance());
}
public void updateTimeout() {
@ -47,4 +52,25 @@ public class LockInfo {
this.timeout = calendar;
}
public long getSession() {
return session;
}
public void setSession(long session) {
this.session = session;
}
public long getTimestamp() {
return timestamp;
}
public long getPreviousTimestamp() {
return previousTimestamp;
}
public void updateTimestamp(@NotNull Mindmap mindmap) {
this.previousTimestamp = this.timestamp;
this.timestamp = mindmap.getLastModificationTime().getTimeInMillis();
}
}

View File

@ -19,6 +19,7 @@
package com.wisemapping.service;
import com.wisemapping.exceptions.AccessDeniedSecurityException;
import com.wisemapping.exceptions.ClientException;
import com.wisemapping.exceptions.LockException;
import com.wisemapping.exceptions.WiseMappingException;
import com.wisemapping.model.Collaborator;
@ -31,13 +32,13 @@ public interface LockManager {
LockInfo getLockInfo(@NotNull Mindmap mindmap);
void updateExpirationTimeout(@NotNull Mindmap mindmap, @NotNull Collaborator user);
LockInfo updateExpirationTimeout(@NotNull Mindmap mindmap, @NotNull Collaborator user,long session);
void unlock(@NotNull Mindmap mindmap, @NotNull Collaborator user) throws LockException, AccessDeniedSecurityException;
boolean isLockedBy(@NotNull Mindmap mindmap, @NotNull Collaborator collaborator);
void lock(@NotNull Mindmap mindmap, @NotNull Collaborator user) throws AccessDeniedSecurityException, LockException;
LockInfo lock(@NotNull Mindmap mindmap, @NotNull Collaborator user, long session) throws WiseMappingException;
void updateLock(boolean value, Mindmap mindmap, User user) throws WiseMappingException;
long generateSession();
}

View File

@ -24,7 +24,6 @@ import com.wisemapping.exceptions.WiseMappingException;
import com.wisemapping.model.CollaborationRole;
import com.wisemapping.model.Collaborator;
import com.wisemapping.model.Mindmap;
import com.wisemapping.model.User;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull;
@ -36,6 +35,15 @@ import java.util.concurrent.ConcurrentHashMap;
* En caso que no sea posible grabar por que se perdio el lock, usar mensaje de error para explicar el por que...
* Mensaje modal explicando que el mapa esta siendo editado, por eso no es posible edilarlo....
* Internacionalizacion de los mensaje ...
* Logout limpiar las sessiones ...
*
* Casos:
* - Usuario pierde el lock:
* - Y grabo con la misma sessions y el timestap ok.
* - Y grabo con la misma session y el timestap esta mal
* - Y grabo con distinta sessions
* -
* - Usuario pierde el lock, pero intenta grabar camio
*/
class LockManagerImpl implements LockManager {
@ -44,30 +52,6 @@ class LockManagerImpl implements LockManager {
final static Timer expirationTimer = new Timer();
final private static Logger logger = Logger.getLogger("com.wisemapping.service.LockManager");
public LockManagerImpl() {
lockInfoByMapId = new ConcurrentHashMap<Integer, LockInfo>();
expirationTimer.schedule(new TimerTask() {
@Override
public void run() {
logger.debug("Lock expiration scheduler started. Current locks:" + lockInfoByMapId.keySet());
final List<Integer> toRemove = new ArrayList<Integer>();
final Set<Integer> mapIds = lockInfoByMapId.keySet();
for (Integer mapId : mapIds) {
final LockInfo lockInfo = lockInfoByMapId.get(mapId);
if (lockInfo.isExpired()) {
toRemove.add(mapId);
}
}
for (Integer mapId : toRemove) {
unlock(mapId);
}
}
}, ONE_MINUTE_MILLISECONDS, ONE_MINUTE_MILLISECONDS);
}
@Override
public boolean isLocked(@NotNull Mindmap mindmap) {
return this.getLockInfo(mindmap) != null;
@ -79,18 +63,21 @@ class LockManagerImpl implements LockManager {
}
@Override
public void updateExpirationTimeout(@NotNull Mindmap mindmap, @NotNull Collaborator user) {
if (this.isLocked(mindmap)) {
final LockInfo lockInfo = this.getLockInfo(mindmap);
if (!lockInfo.getCollaborator().equals(user)) {
throw new IllegalStateException("Could not update map lock timeout if you are not the locking user. User:" + lockInfo.getCollaborator() + ", " + user);
}
lockInfo.updateTimeout();
logger.debug("Timeout updated for:" + mindmap.getId());
}else {
public LockInfo updateExpirationTimeout(@NotNull Mindmap mindmap, @NotNull Collaborator user, long session) {
if (!this.isLocked(mindmap)) {
throw new IllegalStateException("Lock lost for map. No update possible.");
}
final LockInfo result = this.getLockInfo(mindmap);
if (!result.getCollaborator().equals(user)) {
throw new IllegalStateException("Could not update map lock timeout if you are not the locking user. User:" + result.getCollaborator() + ", " + user);
}
result.updateTimeout();
result.setSession(session);
result.updateTimestamp(mindmap);
logger.debug("Timeout updated for:" + mindmap.getId());
return result;
}
@Override
@ -122,7 +109,8 @@ class LockManagerImpl implements LockManager {
}
@Override
public void lock(@NotNull Mindmap mindmap, @NotNull Collaborator user) throws AccessDeniedSecurityException, LockException {
@NotNull
public LockInfo lock(@NotNull Mindmap mindmap, @NotNull Collaborator user, long session) throws WiseMappingException {
if (isLocked(mindmap) && !isLockedBy(mindmap, user)) {
throw new LockException("Invalid lock, this should not happen");
}
@ -131,24 +119,46 @@ class LockManagerImpl implements LockManager {
throw new AccessDeniedSecurityException("Invalid lock, this should not happen");
}
final LockInfo lockInfo = lockInfoByMapId.get(mindmap.getId());
if (lockInfo != null) {
LockInfo result = lockInfoByMapId.get(mindmap.getId());
if (result != null) {
// Update timeout only...
logger.debug("Update timestamp:" + mindmap.getId());
updateExpirationTimeout(mindmap, user);
updateExpirationTimeout(mindmap, user, session);
} else {
logger.debug("Lock map id:" + mindmap.getId());
lockInfoByMapId.put(mindmap.getId(), new LockInfo(user));
result = new LockInfo(user, mindmap, session);
lockInfoByMapId.put(mindmap.getId(), result);
}
return result;
}
@Override
public void updateLock(boolean lock, @NotNull Mindmap mindmap, @NotNull User user) throws WiseMappingException {
if (lock) {
this.lock(mindmap, user);
} else {
this.unlock(mindmap, user);
}
public long generateSession() {
return System.nanoTime();
}
public LockManagerImpl() {
lockInfoByMapId = new ConcurrentHashMap<Integer, LockInfo>();
expirationTimer.schedule(new TimerTask() {
@Override
public void run() {
logger.debug("Lock expiration scheduler started. Current locks:" + lockInfoByMapId.keySet());
final List<Integer> toRemove = new ArrayList<Integer>();
final Set<Integer> mapIds = lockInfoByMapId.keySet();
for (Integer mapId : mapIds) {
final LockInfo lockInfo = lockInfoByMapId.get(mapId);
if (lockInfo.isExpired()) {
toRemove.add(mapId);
}
}
for (Integer mapId : toRemove) {
unlock(mapId);
}
}
}, ONE_MINUTE_MILLISECONDS, ONE_MINUTE_MILLISECONDS);
}
}

View File

@ -100,10 +100,6 @@ public class MindmapServiceImpl
throw new WiseMappingException("The tile can not be empty");
}
// Update edition timeout ...
final LockManager lockManager = this.getLockManager();
lockManager.updateExpirationTimeout(mindMap, Utils.getUser());
mindmapManager.updateMindmap(mindMap, saveHistory);
}

View File

@ -32,6 +32,7 @@
<value>com.wisemapping.rest.model.RestCollaboration</value>
<value>com.wisemapping.rest.model.RestCollaborationList</value>
<value>com.wisemapping.rest.model.RestLogItem</value>
<value>com.wisemapping.rest.model.RestLockInfo</value>
</list>
</property>
</bean>

View File

@ -34,8 +34,16 @@
// Configure designer options ...
var options = loadDesignerOptions();
<c:if test="${!memoryPersistence}">
options.persistenceManager = new mindplot.RESTPersistenceManager("service/maps/{id}/document", "service/maps/{id}/history/latest","service/maps/{id}/lock");
<c:if test="${!memoryPersistence && !readOnlyMode}">
options.persistenceManager = new mindplot.RESTPersistenceManager(
{
saveUrl:"service/maps/{id}/document",
revertUrl:"service/maps/{id}/history/latest",
lockUrl:"service/maps/{id}/lock",
timestamp: ${lockTimestamp},
session: ${lockSession}
}
);
</c:if>
var userOptions = ${mindmap.properties};
options.zoom = userOptions.zoom;