- Finish exclusive locking support.

This commit is contained in:
Paulo Gustavo Veiga 2012-09-30 17:15:01 -03:00 committed by Paulo Gustavo Veiga
parent e5e2e86fce
commit a179875fee
24 changed files with 549 additions and 104 deletions

View File

@ -18,28 +18,28 @@
mindplot.LocalStorageManager = new Class({ mindplot.LocalStorageManager = new Class({
Extends:mindplot.PersistenceManager, Extends:mindplot.PersistenceManager,
initialize: function() { initialize:function () {
this.parent(); this.parent();
}, },
saveMapXml : function(mapId, mapXml, pref, saveHistory, events) { saveMapXml:function (mapId, mapXml, pref, saveHistory, events) {
localStorage.setItem(mapId + "-xml", mapXml); localStorage.setItem(mapId + "-xml", mapXml);
events.onSuccess(); events.onSuccess();
}, },
discardChanges : function(mapId) { discardChanges:function (mapId) {
localStorage.removeItem(mapId + "-xml"); localStorage.removeItem(mapId + "-xml");
}, },
loadMapDom : function(mapId) { loadMapDom:function (mapId) {
var xml = localStorage.getItem(mapId + "-xml"); var xml = localStorage.getItem(mapId + "-xml");
if (xml == null) { if (xml == null) {
// Let's try to open one from the local directory ... // Let's try to open one from the local directory ...
var xmlRequest = new Request({ var xmlRequest = new Request({
url: 'samples/' + mapId + '.xml', url:'samples/' + mapId + '.xml',
method: 'get', method:'get',
async: false, async:false,
onSuccess: function(responseText) { onSuccess:function (responseText) {
xml = responseText; xml = responseText;
} }
}); });
@ -54,6 +54,10 @@ mindplot.LocalStorageManager = new Class({
var parser = new DOMParser(); var parser = new DOMParser();
return parser.parseFromString(xml, "text/xml"); return parser.parseFromString(xml, "text/xml");
},
unlockMap:function (mindmap) {
// Ignore, no implementation required ...
} }
} }
); );

View File

@ -31,7 +31,7 @@ mindplot.PersistenceManager = new Class({
}, },
save:function (mindmap, editorProperties, saveHistory, events) { save:function (mindmap, editorProperties, saveHistory, events, sync) {
$assert(mindmap, "mindmap can not be null"); $assert(mindmap, "mindmap can not be null");
$assert(editorProperties, "editorProperties can not be null"); $assert(editorProperties, "editorProperties can not be null");
@ -44,7 +44,7 @@ mindplot.PersistenceManager = new Class({
var pref = JSON.encode(editorProperties); var pref = JSON.encode(editorProperties);
try { try {
this.saveMapXml(mapId, mapXml, pref, saveHistory, events); this.saveMapXml(mapId, mapXml, pref, saveHistory, events,sync);
} catch (e) { } catch (e) {
console.log(e); console.log(e);
events.onError(); events.onError();
@ -58,15 +58,19 @@ mindplot.PersistenceManager = new Class({
}, },
discardChanges:function (mapId) { discardChanges:function (mapId) {
throw "Method must be implemented"; throw new Error("Method must be implemented");
}, },
loadMapDom:function (mapId) { loadMapDom:function (mapId) {
throw "Method must be implemented"; throw new Error("Method must be implemented");
}, },
saveMapXml:function (mapId, mapXml, pref, saveHistory, events) { saveMapXml:function (mapId, mapXml, pref, saveHistory, events,sync) {
throw "Method must be implemented"; throw new Error("Method must be implemented");
},
unlockMap:function (mindmap) {
throw new Error("Method must be implemented");
} }
}); });

View File

@ -18,15 +18,17 @@
mindplot.RESTPersistenceManager = new Class({ mindplot.RESTPersistenceManager = new Class({
Extends:mindplot.PersistenceManager, Extends:mindplot.PersistenceManager,
initialize:function (saveUrl, revertUrl) { initialize:function (saveUrl, revertUrl, lockUrl) {
this.parent(); this.parent();
$assert(saveUrl, "saveUrl can not be null"); $assert(saveUrl, "saveUrl can not be null");
$assert(revertUrl, "revertUrl can not be null"); $assert(revertUrl, "revertUrl can not be null");
this.saveUrl = saveUrl; this.saveUrl = saveUrl;
this.revertUrl = revertUrl; this.revertUrl = revertUrl;
this.lockUrl = lockUrl;
this.timestamp = null;
}, },
saveMapXml:function (mapId, mapXml, pref, saveHistory, events) { saveMapXml:function (mapId, mapXml, pref, saveHistory, events, sync) {
var data = { var data = {
id:mapId, id:mapId,
@ -34,12 +36,17 @@ mindplot.RESTPersistenceManager = new Class({
properties:pref properties:pref
}; };
var persistence = this;
var query = "minor=" + !saveHistory;
query = query + (this.timestamp ? "&timestamp=" + this.timestamp : "");
var request = new Request({ var request = new Request({
url:this.saveUrl.replace("{id}", mapId) + "?minor=" + !saveHistory, url:this.saveUrl.replace("{id}", mapId) + "?" + query,
method:'put', method:'put',
async:!sync,
onSuccess:function (responseText, responseXML) { onSuccess:function (responseText, responseXML) {
events.onSuccess(); events.onSuccess();
persistence.timestamp = responseText;
}, },
onException:function (headerName, value) { onException:function (headerName, value) {
events.onError(); events.onError();
@ -81,8 +88,27 @@ mindplot.RESTPersistenceManager = new Class({
urlEncoded:false urlEncoded:false
}); });
request.post(); request.post();
} },
unlockMap:function (mindmap) {
var mapId = mindmap.getId();
var request = new Request({
url:this.lockUrl.replace("{id}", mapId),
async:false,
method:'put',
onSuccess:function () {
},
onException:function () {
},
onFailure:function () {
},
headers:{"Content-Type":"text/plain"},
emulation:false,
urlEncoded:false
});
request.put("false");
}
} }
); );

View File

@ -40,7 +40,7 @@ mindplot.widget.IMenu = new Class({
}); });
}, },
discardChanges:function () { discardChanges:function (designer) {
// Avoid autosave before leaving the page .... // Avoid autosave before leaving the page ....
this.setRequireChange(false); this.setRequireChange(false);
@ -49,12 +49,21 @@ mindplot.widget.IMenu = new Class({
var mindmap = designer.getMindmap(); var mindmap = designer.getMindmap();
persistenceManager.discardChanges(mindmap.getId()); persistenceManager.discardChanges(mindmap.getId());
// Unlock map ...
this.unlockMap(designer);
// Reload the page ... // Reload the page ...
window.location.reload(); window.location.reload();
}, },
save:function (saveElem, designer, saveHistory) { unlockMap:function (designer) {
var mindmap = designer.getMindmap();
var persistenceManager = mindplot.PersistenceManager.getInstance();
persistenceManager.unlockMap(mindmap);
},
save:function (saveElem, designer, saveHistory, sync) {
// Load map content ... // Load map content ...
var mindmap = designer.getMindmap(); var mindmap = designer.getMindmap();
var mindmapProp = designer.getMindmapProperties(); var mindmapProp = designer.getMindmapProperties();
@ -88,7 +97,8 @@ mindplot.widget.IMenu = new Class({
$notify(msg); $notify(msg);
} }
} }
}); }, sync);
}, },
isSaveRequired:function () { isSaveRequired:function () {

View File

@ -325,10 +325,12 @@ mindplot.widget.Menu = new Class({
if (!readOnly) { if (!readOnly) {
// To prevent the user from leaving the page with changes ... // To prevent the user from leaving the page with changes ...
$(window).addEvent('beforeunload', function () { Element.NativeEvents.unload = 2;
$(window).addEvent('unload', function () {
if (this.isSaveRequired()) { if (this.isSaveRequired()) {
this.save(saveElem, designer, false); this.save(saveElem, designer, false, true);
} }
this.unlockMap(designer);
}.bind(this)); }.bind(this));
// Autosave on a fixed period of time ... // Autosave on a fixed period of time ...
@ -343,29 +345,11 @@ mindplot.widget.Menu = new Class({
var discardElem = $('discard'); var discardElem = $('discard');
if (discardElem) { if (discardElem) {
this._addButton('discard', false, false, function () { this._addButton('discard', false, false, function () {
this.discardChanges(); this.discardChanges(designer);
}.bind(this)); }.bind(this));
this._registerTooltip('discard', $msg('DISCARD_CHANGES')); this._registerTooltip('discard', $msg('DISCARD_CHANGES'));
} }
var tagElem = $('tagIt');
if (tagElem) {
this._addButton('tagIt', false, false, function () {
var reqDialog = new MooDialog.Request('c/tags?mapId=' + mapId, null,
{'class':'modalDialog tagItModalDialog',
closeButton:true,
destroyOnClose:true,
title:'Tags'
});
reqDialog.setRequestOptions({
onRequest:function () {
reqDialog.setContent($msg('LOADING'));
}
});
});
this._registerTooltip('tagIt', "Tag");
}
var shareElem = $('shareIt'); var shareElem = $('shareIt');
if (shareElem) { if (shareElem) {
this._addButton('shareIt', false, false, function () { this._addButton('shareIt', false, false, function () {

View File

@ -0,0 +1,35 @@
/*
* 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 LockException
extends ClientException
{
public LockException(@NotNull String message) {
super(message);
}
@NotNull
@Override
protected String getMsgBundleKey() {
return null; //To change body of implemented methods use File | Settings | File Templates.
}
}

View File

@ -18,12 +18,21 @@
package com.wisemapping.exceptions; package com.wisemapping.exceptions;
import org.jetbrains.annotations.NotNull;
public class UnexpectedArgumentException public class MindmapOutdatedException
extends Exception extends ClientException
{ {
public UnexpectedArgumentException(String msg) public static final String MSG_KEY = "MINDMAP_TIMESTAMP_OUTDATED";
public MindmapOutdatedException(@NotNull String msg)
{ {
super(msg); super(msg);
} }
@NotNull
@Override
protected String getMsgBundleKey() {
return MSG_KEY;
}
} }

View File

@ -138,10 +138,6 @@ public class Mindmap {
return lastModificationTime; return lastModificationTime;
} }
public Date getLastModificationDate() {
return new Date();
}
public void setLastModificationTime(Calendar lastModificationTime) { public void setLastModificationTime(Calendar lastModificationTime) {
this.lastModificationTime = lastModificationTime; this.lastModificationTime = lastModificationTime;
} }

View File

@ -20,6 +20,8 @@ package com.wisemapping.model;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
import javax.servlet.http.HttpSessionBindingEvent;
import javax.servlet.http.HttpSessionBindingListener;
import java.io.Serializable; import java.io.Serializable;
import java.util.*; import java.util.*;

View File

@ -19,12 +19,15 @@
package com.wisemapping.ncontroller; package com.wisemapping.ncontroller;
import com.wisemapping.exceptions.AccessDeniedSecurityException;
import com.wisemapping.exceptions.LockException;
import com.wisemapping.exceptions.WiseMappingException; import com.wisemapping.exceptions.WiseMappingException;
import com.wisemapping.model.CollaborationRole; import com.wisemapping.model.CollaborationRole;
import com.wisemapping.model.Mindmap; import com.wisemapping.model.Mindmap;
import com.wisemapping.model.MindMapHistory; import com.wisemapping.model.MindMapHistory;
import com.wisemapping.model.User; import com.wisemapping.model.User;
import com.wisemapping.security.Utils; import com.wisemapping.security.Utils;
import com.wisemapping.service.LockManager;
import com.wisemapping.service.MindmapService; import com.wisemapping.service.MindmapService;
import com.wisemapping.view.MindMapBean; import com.wisemapping.view.MindMapBean;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
@ -140,33 +143,47 @@ public class MindmapController {
} }
@RequestMapping(value = "maps/{id}/edit", method = RequestMethod.GET) @RequestMapping(value = "maps/{id}/edit", method = RequestMethod.GET)
public String showMindmapEditorPage(@PathVariable int id, @NotNull Model model) { public String showMindmapEditorPage(@PathVariable int id, @NotNull Model model) throws WiseMappingException {
return showEditorPage(id, model, true);
}
private String showEditorPage(int id, @NotNull final Model model, boolean requiresLock) throws AccessDeniedSecurityException, LockException {
final MindMapBean mindmapBean = findMindmapBean(id); final MindMapBean mindmapBean = findMindmapBean(id);
final Mindmap mindmap = mindmapBean.getDelegated(); final Mindmap mindmap = mindmapBean.getDelegated();
final User collaborator = Utils.getUser();
final Locale locale = LocaleContextHolder.getLocale();
// Is the mindmap locked ?.
boolean readOnlyMode = !requiresLock || !mindmap.hasPermissions(collaborator, CollaborationRole.EDITOR);
if (!readOnlyMode) {
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);
}
}
// Set render attributes ...
model.addAttribute("mindmap", mindmapBean); model.addAttribute("mindmap", mindmapBean);
// Configure default locale for the editor ... // Configure default locale for the editor ...
final Locale locale = LocaleContextHolder.getLocale();
model.addAttribute("locale", locale.toString().toLowerCase()); model.addAttribute("locale", locale.toString().toLowerCase());
final User collaborator = Utils.getUser();
model.addAttribute("principal", collaborator); model.addAttribute("principal", collaborator);
model.addAttribute("readOnlyMode", !mindmap.hasPermissions(collaborator, CollaborationRole.EDITOR)); model.addAttribute("readOnlyMode", readOnlyMode);
return "mindmapEditor"; return "mindmapEditor";
} }
@RequestMapping(value = "maps/{id}/view", method = RequestMethod.GET) @RequestMapping(value = "maps/{id}/view", method = RequestMethod.GET)
public String showMindmapViewerPage(@PathVariable int id, @NotNull Model model) { public String showMindmapViewerPage(@PathVariable int id, @NotNull Model model) throws LockException, AccessDeniedSecurityException {
final String result = showMindmapEditorPage(id, model); return showEditorPage(id, model, false);
model.addAttribute("readOnlyMode", true);
return result;
} }
@RequestMapping(value = "maps/{id}/try", method = RequestMethod.GET) @RequestMapping(value = "maps/{id}/try", method = RequestMethod.GET)
public String showMindmapTryPage(@PathVariable int id, @NotNull Model model) { public String showMindmapTryPage(@PathVariable int id, @NotNull Model model) throws LockException, AccessDeniedSecurityException {
final String result = showMindmapEditorPage(id, model); final String result = showEditorPage(id, model, false);
model.addAttribute("memoryPersistence", true); model.addAttribute("memoryPersistence", true);
model.addAttribute("readOnlyMode", false);
return result; return result;
} }
@ -213,11 +230,7 @@ public class MindmapController {
} }
private Mindmap findMindmap(long mapId) { private Mindmap findMindmap(long mapId) {
final Mindmap mindmap = mindmapService.findMindmapById((int) mapId); return mindmapService.findMindmapById((int) mapId);
if (mindmap == null) {
throw new IllegalArgumentException("Mindmap could not be found");
}
return mindmap;
} }
private MindMapBean findMindmapBean(long mapId) { private MindMapBean findMindmapBean(long mapId) {

View File

@ -24,6 +24,7 @@ import com.wisemapping.mail.NotificationService;
import com.wisemapping.model.User; import com.wisemapping.model.User;
import com.wisemapping.rest.model.RestErrors; import com.wisemapping.rest.model.RestErrors;
import com.wisemapping.security.Utils; import com.wisemapping.security.Utils;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
@ -41,6 +42,8 @@ import java.util.Locale;
public class BaseController { public class BaseController {
final protected static Logger logger = Logger.getLogger("com.wisemapping.rest");
@Qualifier("messageSource") @Qualifier("messageSource")
@Autowired @Autowired
private ResourceBundleMessageSource messageSource; private ResourceBundleMessageSource messageSource;

View File

@ -20,6 +20,7 @@ package com.wisemapping.rest;
import com.wisemapping.exceptions.ImportUnexpectedException; import com.wisemapping.exceptions.ImportUnexpectedException;
import com.wisemapping.exceptions.MindmapOutdatedException;
import com.wisemapping.exceptions.WiseMappingException; import com.wisemapping.exceptions.WiseMappingException;
import com.wisemapping.importer.ImportFormat; import com.wisemapping.importer.ImportFormat;
import com.wisemapping.importer.Importer; import com.wisemapping.importer.Importer;
@ -29,8 +30,10 @@ import com.wisemapping.model.*;
import com.wisemapping.rest.model.*; import com.wisemapping.rest.model.*;
import com.wisemapping.security.Utils; import com.wisemapping.security.Utils;
import com.wisemapping.service.CollaborationException; import com.wisemapping.service.CollaborationException;
import com.wisemapping.service.LockManager;
import com.wisemapping.service.MindmapService; import com.wisemapping.service.MindmapService;
import com.wisemapping.validator.MapInfoValidator; import com.wisemapping.validator.MapInfoValidator;
import org.apache.log4j.Logger;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
@ -49,6 +52,7 @@ import java.util.*;
@Controller @Controller
public class MindmapController extends BaseController { public class MindmapController extends BaseController {
public static final String LATEST_HISTORY_REVISION = "latest"; public static final String LATEST_HISTORY_REVISION = "latest";
@Qualifier("mindmapService") @Qualifier("mindmapService")
@Autowired @Autowired
@ -136,8 +140,8 @@ 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"}) @RequestMapping(method = RequestMethod.PUT, value = "/maps/{id}/document", consumes = {"application/xml", "application/json"}, produces = {"application/json", "text/html", "application/xml"})
@ResponseStatus(value = HttpStatus.NO_CONTENT) @ResponseBody
public void updateDocument(@RequestBody RestMindmap restMindmap, @PathVariable int id, @RequestParam(required = false) boolean minor) throws WiseMappingException, IOException { public long updateDocument(@RequestBody RestMindmap restMindmap, @PathVariable int id, @RequestParam(required = false) boolean minor, @RequestParam(required = false) Long timestamp) throws WiseMappingException, IOException {
final Mindmap mindmap = mindmapService.findMindmapById(id); final Mindmap mindmap = mindmapService.findMindmapById(id);
final User user = Utils.getUser(); final User user = Utils.getUser();
@ -148,6 +152,11 @@ public class MindmapController extends BaseController {
throw new IllegalArgumentException("Map properties can not be null"); 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);
}
// Update collaboration properties ... // Update collaboration properties ...
final CollaborationProperties collaborationProperties = mindmap.findCollaborationProperties(user); final CollaborationProperties collaborationProperties = mindmap.findCollaborationProperties(user);
collaborationProperties.setMindmapProperties(properties); collaborationProperties.setMindmapProperties(properties);
@ -160,7 +169,11 @@ public class MindmapController extends BaseController {
mindmap.setXmlStr(xml); mindmap.setXmlStr(xml);
// Update map ... // Update map ...
logger.debug("Mindmap save completed:" + restMindmap.getXml());
saveMindmap(minor, mindmap, user); saveMindmap(minor, mindmap, user);
// Return last update timestamp ...
return mindmap.getLastModificationTime().getTimeInMillis();
} }
/** /**
@ -317,6 +330,14 @@ public class MindmapController extends BaseController {
} }
@RequestMapping(method = RequestMethod.DELETE, value = "/maps/{id}")
@ResponseStatus(value = HttpStatus.NO_CONTENT)
public void updateMap(@PathVariable int id) throws IOException, WiseMappingException {
final User user = Utils.getUser();
final Mindmap mindmap = mindmapService.findMindmapById(id);
mindmapService.removeMindmap(mindmap, user);
}
@RequestMapping(method = RequestMethod.PUT, value = "/maps/{id}/starred", consumes = {"text/plain"}, produces = {"application/json", "text/html", "application/xml"}) @RequestMapping(method = RequestMethod.PUT, value = "/maps/{id}/starred", consumes = {"text/plain"}, produces = {"application/json", "text/html", "application/xml"})
@ResponseStatus(value = HttpStatus.NO_CONTENT) @ResponseStatus(value = HttpStatus.NO_CONTENT)
public void updateStarredState(@RequestBody String value, @PathVariable int id) throws WiseMappingException { public void updateStarredState(@RequestBody String value, @PathVariable int id) throws WiseMappingException {
@ -334,12 +355,13 @@ public class MindmapController extends BaseController {
mindmapService.updateCollaboration(user, collaboration); mindmapService.updateCollaboration(user, collaboration);
} }
@RequestMapping(method = RequestMethod.DELETE, value = "/maps/{id}") @RequestMapping(method = RequestMethod.PUT, value = "/maps/{id}/lock", consumes = {"text/plain"}, produces = {"application/json", "text/html", "application/xml"})
@ResponseStatus(value = HttpStatus.NO_CONTENT) @ResponseStatus(value = HttpStatus.NO_CONTENT)
public void updateMap(@PathVariable int id) throws IOException, WiseMappingException { public void updateMapLock(@RequestBody String value, @PathVariable int id) throws IOException, WiseMappingException {
final User user = Utils.getUser(); final User user = Utils.getUser();
final LockManager lockManager = mindmapService.getLockManager();
final Mindmap mindmap = mindmapService.findMindmapById(id); final Mindmap mindmap = mindmapService.findMindmapById(id);
mindmapService.removeMindmap(mindmap, user); lockManager.updateLock(Boolean.parseBoolean(value), mindmap, user);
} }
@RequestMapping(method = RequestMethod.DELETE, value = "/maps/batch") @RequestMapping(method = RequestMethod.DELETE, value = "/maps/batch")

View File

@ -0,0 +1,55 @@
package com.wisemapping.rest.model;
import com.wisemapping.model.Collaborator;
import com.wisemapping.model.User;
import com.wisemapping.service.LockInfo;
import org.codehaus.jackson.annotate.JsonAutoDetect;
import org.codehaus.jackson.annotate.JsonIgnore;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlRootElement;
import java.util.Calendar;
import java.util.Date;
import java.util.Set;
@XmlRootElement(name = "lock")
@XmlAccessorType(XmlAccessType.PROPERTY)
@JsonAutoDetect(
fieldVisibility = JsonAutoDetect.Visibility.NONE,
getterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY,
isGetterVisibility = JsonAutoDetect.Visibility.PUBLIC_ONLY)
@JsonIgnoreProperties(ignoreUnknown = true)
public class RestMindmapLock {
@NotNull
private Collaborator user;
@Nullable
private LockInfo lockInfo;
public RestMindmapLock(@Nullable LockInfo lockInfo, @NotNull Collaborator collaborator) {
this.lockInfo = lockInfo;
this.user = collaborator;
}
public boolean isLocked() {
return lockInfo != null;
}
public void setLocked(boolean locked) {
// Ignore ...
}
public boolean isLockedByMe() {
return isLocked() && lockInfo != null && lockInfo.getCollaborator().equals(user);
}
public void setLockedByMe(boolean lockedForMe) {
// Ignore ...
}
}

View File

@ -1,3 +1,21 @@
/*
* 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.security; package com.wisemapping.security;

View File

@ -22,7 +22,6 @@ import com.wisemapping.model.Collaborator;
import com.wisemapping.model.Mindmap; import com.wisemapping.model.Mindmap;
import com.wisemapping.model.User; import com.wisemapping.model.User;
import com.wisemapping.exceptions.AccessDeniedSecurityException; import com.wisemapping.exceptions.AccessDeniedSecurityException;
import com.wisemapping.exceptions.UnexpectedArgumentException;
import com.wisemapping.security.Utils; import com.wisemapping.security.Utils;
import com.wisemapping.service.MindmapService; import com.wisemapping.service.MindmapService;
import org.aopalliance.intercept.MethodInvocation; import org.aopalliance.intercept.MethodInvocation;
@ -31,7 +30,7 @@ import org.jetbrains.annotations.Nullable;
public abstract class BaseSecurityAdvice { public abstract class BaseSecurityAdvice {
private MindmapService mindmapService = null; private MindmapService mindmapService = null;
public void checkRole(MethodInvocation methodInvocation) throws UnexpectedArgumentException, AccessDeniedSecurityException { public void checkRole(MethodInvocation methodInvocation) throws AccessDeniedSecurityException {
final User user = Utils.getUser(); final User user = Utils.getUser();
final Object argument = methodInvocation.getArguments()[0]; final Object argument = methodInvocation.getArguments()[0];
boolean isAllowed; boolean isAllowed;
@ -44,7 +43,7 @@ public abstract class BaseSecurityAdvice {
// Read operation find on the user are allowed ... // Read operation find on the user are allowed ...
isAllowed = user.equals(argument); isAllowed = user.equals(argument);
} else { } else {
throw new UnexpectedArgumentException("Argument " + argument); throw new IllegalArgumentException("Argument " + argument);
} }
if (!isAllowed) { if (!isAllowed) {

View File

@ -0,0 +1,50 @@
/*
* 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.service;
import com.wisemapping.model.Collaborator;
import org.jetbrains.annotations.NotNull;
import java.util.Calendar;
public class LockInfo {
final private Collaborator collaborator;
private Calendar timeout;
private static int EXPIRATION_MIN = 25;
public LockInfo(@NotNull Collaborator collaborator) {
this.collaborator = collaborator;
this.updateTimeout();
}
public Collaborator getCollaborator() {
return collaborator;
}
public boolean isExpired() {
return timeout.before(Calendar.getInstance());
}
public void updateTimeout() {
final Calendar calendar = Calendar.getInstance();
calendar.add(Calendar.MINUTE, EXPIRATION_MIN);
this.timeout = calendar;
}
}

View File

@ -0,0 +1,43 @@
/*
* 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.service;
import com.wisemapping.exceptions.AccessDeniedSecurityException;
import com.wisemapping.exceptions.LockException;
import com.wisemapping.exceptions.WiseMappingException;
import com.wisemapping.model.Collaborator;
import com.wisemapping.model.Mindmap;
import com.wisemapping.model.User;
import org.jetbrains.annotations.NotNull;
public interface LockManager {
boolean isLocked(@NotNull Mindmap mindmap);
LockInfo getLockInfo(@NotNull Mindmap mindmap);
void updateExpirationTimeout(@NotNull Mindmap mindmap, @NotNull Collaborator user);
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;
void updateLock(boolean value, Mindmap mindmap, User user) throws WiseMappingException;
}

View File

@ -0,0 +1,153 @@
/*
* 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.service;
import com.wisemapping.exceptions.AccessDeniedSecurityException;
import com.wisemapping.exceptions.LockException;
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;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
/*
* Refresh page should not lost the lock.
* 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....
*/
class LockManagerImpl implements LockManager {
public static final int ONE_MINUTE_MILLISECONDS = 1000 * 60;
final Map<Integer, LockInfo> lockInfoByMapId;
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;
}
@Override
public LockInfo getLockInfo(@NotNull Mindmap mindmap) {
return lockInfoByMapId.get(mindmap.getId());
}
@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 {
throw new IllegalStateException("Lock lost for map. No update possible.");
}
}
@Override
public void unlock(@NotNull Mindmap mindmap, @NotNull Collaborator user) throws LockException, AccessDeniedSecurityException {
if (isLocked(mindmap) && !isLockedBy(mindmap, user)) {
throw new LockException("Lock can be only revoked by the locker.");
}
if (!mindmap.hasPermissions(user, CollaborationRole.EDITOR)) {
throw new AccessDeniedSecurityException("Invalid lock, this should not happen");
}
this.unlock(mindmap.getId());
}
private void unlock(int mapId) {
logger.debug("Unlock map id:" + mapId);
lockInfoByMapId.remove(mapId);
}
@Override
public boolean isLockedBy(@NotNull Mindmap mindmap, @NotNull Collaborator collaborator) {
boolean result = false;
final LockInfo lockInfo = this.getLockInfo(mindmap);
if (lockInfo != null && lockInfo.getCollaborator().equals(collaborator)) {
result = true;
}
return result;
}
@Override
public void lock(@NotNull Mindmap mindmap, @NotNull Collaborator user) throws AccessDeniedSecurityException, LockException {
if (isLocked(mindmap) && !isLockedBy(mindmap, user)) {
throw new LockException("Invalid lock, this should not happen");
}
if (!mindmap.hasPermissions(user, CollaborationRole.EDITOR)) {
throw new AccessDeniedSecurityException("Invalid lock, this should not happen");
}
final LockInfo lockInfo = lockInfoByMapId.get(mindmap.getId());
if (lockInfo != null) {
// Update timeout only...
logger.debug("Update timestamp:" + mindmap.getId());
updateExpirationTimeout(mindmap, user);
} else {
logger.debug("Lock map id:" + mindmap.getId());
lockInfoByMapId.put(mindmap.getId(), new LockInfo(user));
}
}
@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);
}
}
}

View File

@ -62,4 +62,6 @@ public interface MindmapService {
MindMapHistory findMindmapHistory(int id, int hid) throws WiseMappingException; MindMapHistory findMindmapHistory(int id, int hid) throws WiseMappingException;
void updateCollaboration(@NotNull Collaborator collaborator, @NotNull Collaboration collaboration) throws WiseMappingException; void updateCollaboration(@NotNull Collaborator collaborator, @NotNull Collaboration collaboration) throws WiseMappingException;
LockManager getLockManager();
} }

View File

@ -45,6 +45,11 @@ public class MindmapServiceImpl
private NotificationService notificationService; private NotificationService notificationService;
private String adminUser; private String adminUser;
final private LockManager lockManager;
public MindmapServiceImpl() {
this.lockManager = new LockManagerImpl();
}
@Override @Override
public boolean hasPermissions(@Nullable User user, int mapId, @NotNull CollaborationRole grantedRole) { public boolean hasPermissions(@Nullable User user, int mapId, @NotNull CollaborationRole grantedRole) {
@ -94,6 +99,11 @@ public class MindmapServiceImpl
if (mindMap.getTitle() == null || mindMap.getTitle().length() == 0) { if (mindMap.getTitle() == null || mindMap.getTitle().length() == 0) {
throw new WiseMappingException("The tile can not be empty"); 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); mindmapManager.updateMindmap(mindMap, saveHistory);
} }
@ -264,6 +274,12 @@ public class MindmapServiceImpl
mindmapManager.updateCollaboration(collaboration); mindmapManager.updateCollaboration(collaboration);
} }
@Override
@NotNull
public LockManager getLockManager() {
return this.lockManager;
}
private Collaboration getCollaborationBy(String email, Set<Collaboration> collaborations) { private Collaboration getCollaborationBy(String email, Set<Collaboration> collaborations) {
Collaboration collaboration = null; Collaboration collaboration = null;

View File

@ -249,7 +249,7 @@ DIRECT_LINK_EXPLANATION=Copy and paste the link below to share your map with col
TEMPORAL_PASSWORD_SENT=Your temporal password has been sent TEMPORAL_PASSWORD_SENT=Your temporal password has been sent
TEMPORAL_PASSWORD_SENT_DETAILS=We've sent you an email that will allow you to reset your password. Please check your email now. TEMPORAL_PASSWORD_SENT_DETAILS=We've sent you an email that will allow you to reset your password. Please check your email now.
TEMPORAL_PASSWORD_SENT_SUPPORT=If you have any problem receiving the email, contact us to <a href\="mailto\:support@wisemapping.com">support@wisemapping.com </a> TEMPORAL_PASSWORD_SENT_SUPPORT=If you have any problem receiving the email, contact us to <a href\="mailto\:support@wisemapping.com">support@wisemapping.com </a>
MINDMAP_TIMESTAMP_OUTDATED=It's not possible to save your changes because your mindmap is out of date. Refresh the page and try again.

View File

@ -1,8 +1,9 @@
log4j.rootLogger=WARN, stdout, R log4j.rootLogger=WARN, stdout, R
log4j.logger.com.wisemapping.service.LockManager=DEBUG,stdout,R
log4j.logger.com.wisemapping=WARN,stdout,R log4j.logger.com.wisemapping=WARN,stdout,R
log4j.logger.org.springframework=WARN,stdout,R log4j.logger.org.springframework=WARN,stdout,R
log4j.logger.org.codehaus.jackson=WARN,stdout,R log4j.logger.org.codehaus.jackson=WARN,stdout,R
log4j.logger.org.hibernate=DEBUG,stdout,R log4j.logger.org.hibernate=WARN,stdout,R
log4j.logger.org.hibernate.SQL=true log4j.logger.org.hibernate.SQL=true

View File

@ -35,7 +35,7 @@
// Configure designer options ... // Configure designer options ...
var options = loadDesignerOptions(); var options = loadDesignerOptions();
<c:if test="${!memoryPersistence}"> <c:if test="${!memoryPersistence}">
options.persistenceManager = new mindplot.RESTPersistenceManager("service/maps/{id}/document", "service/maps/{id}/history/latest"); options.persistenceManager = new mindplot.RESTPersistenceManager("service/maps/{id}/document", "service/maps/{id}/history/latest","service/maps/{id}/lock");
</c:if> </c:if>
var userOptions = ${mindmap.properties}; var userOptions = ${mindmap.properties};
options.zoom = userOptions.zoom; options.zoom = userOptions.zoom;