From a179875feed0eebed7ad33933637c0d92ad24f77 Mon Sep 17 00:00:00 2001 From: Paulo Gustavo Veiga Date: Sun, 30 Sep 2012 17:15:01 -0300 Subject: [PATCH] - Finish exclusive locking support. --- .../main/javascript/LocalStorageManager.js | 20 ++- .../src/main/javascript/PersistenceManager.js | 16 +- .../main/javascript/RestPersistenceManager.js | 36 ++++- mindplot/src/main/javascript/widget/IMenu.js | 16 +- mindplot/src/main/javascript/widget/Menu.js | 26 +-- .../wisemapping/exceptions/LockException.java | 35 ++++ ...ion.java => MindmapOutdatedException.java} | 67 ++++---- .../java/com/wisemapping/model/Mindmap.java | 4 - .../main/java/com/wisemapping/model/User.java | 2 + .../ncontroller/MindmapController.java | 45 ++++-- .../com/wisemapping/rest/BaseController.java | 3 + .../wisemapping/rest/MindmapController.java | 32 +++- .../rest/model/RestMindmapLock.java | 55 +++++++ .../security/AuthenticationProvider.java | 18 +++ .../security/aop/BaseSecurityAdvice.java | 5 +- .../com/wisemapping/service/LockInfo.java | 50 ++++++ .../com/wisemapping/service/LockManager.java | 43 +++++ .../wisemapping/service/LockManagerImpl.java | 153 ++++++++++++++++++ .../wisemapping/service/MindmapService.java | 2 + .../service/MindmapServiceImpl.java | 16 ++ .../src/main/resources/messages_en.properties | 2 +- .../webapp/WEB-INF/classes/log4j.properties | 3 +- .../src/main/webapp/jsp/mindmapEditor.jsp | 2 +- .../resources/data/freemind/node-styles.mmr | 2 +- 24 files changed, 549 insertions(+), 104 deletions(-) create mode 100755 wise-webapp/src/main/java/com/wisemapping/exceptions/LockException.java rename wise-webapp/src/main/java/com/wisemapping/exceptions/{UnexpectedArgumentException.java => MindmapOutdatedException.java} (71%) create mode 100644 wise-webapp/src/main/java/com/wisemapping/rest/model/RestMindmapLock.java create mode 100644 wise-webapp/src/main/java/com/wisemapping/service/LockInfo.java create mode 100644 wise-webapp/src/main/java/com/wisemapping/service/LockManager.java create mode 100644 wise-webapp/src/main/java/com/wisemapping/service/LockManagerImpl.java diff --git a/mindplot/src/main/javascript/LocalStorageManager.js b/mindplot/src/main/javascript/LocalStorageManager.js index 201b6b1c..a8ed69ee 100644 --- a/mindplot/src/main/javascript/LocalStorageManager.js +++ b/mindplot/src/main/javascript/LocalStorageManager.js @@ -18,28 +18,28 @@ mindplot.LocalStorageManager = new Class({ Extends:mindplot.PersistenceManager, - initialize: function() { + initialize:function () { this.parent(); }, - saveMapXml : function(mapId, mapXml, pref, saveHistory, events) { + saveMapXml:function (mapId, mapXml, pref, saveHistory, events) { localStorage.setItem(mapId + "-xml", mapXml); events.onSuccess(); }, - discardChanges : function(mapId) { + discardChanges:function (mapId) { localStorage.removeItem(mapId + "-xml"); }, - loadMapDom : function(mapId) { + loadMapDom:function (mapId) { var xml = localStorage.getItem(mapId + "-xml"); if (xml == null) { // Let's try to open one from the local directory ... var xmlRequest = new Request({ - url: 'samples/' + mapId + '.xml', - method: 'get', - async: false, - onSuccess: function(responseText) { + url:'samples/' + mapId + '.xml', + method:'get', + async:false, + onSuccess:function (responseText) { xml = responseText; } }); @@ -54,6 +54,10 @@ mindplot.LocalStorageManager = new Class({ var parser = new DOMParser(); return parser.parseFromString(xml, "text/xml"); + }, + + unlockMap:function (mindmap) { + // Ignore, no implementation required ... } } ); diff --git a/mindplot/src/main/javascript/PersistenceManager.js b/mindplot/src/main/javascript/PersistenceManager.js index d7d11948..84bc791d 100644 --- a/mindplot/src/main/javascript/PersistenceManager.js +++ b/mindplot/src/main/javascript/PersistenceManager.js @@ -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(editorProperties, "editorProperties can not be null"); @@ -44,7 +44,7 @@ mindplot.PersistenceManager = new Class({ var pref = JSON.encode(editorProperties); try { - this.saveMapXml(mapId, mapXml, pref, saveHistory, events); + this.saveMapXml(mapId, mapXml, pref, saveHistory, events,sync); } catch (e) { console.log(e); events.onError(); @@ -58,15 +58,19 @@ mindplot.PersistenceManager = new Class({ }, discardChanges:function (mapId) { - throw "Method must be implemented"; + throw new Error("Method must be implemented"); }, loadMapDom:function (mapId) { - throw "Method must be implemented"; + throw new Error("Method must be implemented"); }, - saveMapXml:function (mapId, mapXml, pref, saveHistory, events) { - throw "Method must be implemented"; + saveMapXml:function (mapId, mapXml, pref, saveHistory, events,sync) { + throw new Error("Method must be implemented"); + }, + + unlockMap:function (mindmap) { + throw new Error("Method must be implemented"); } }); diff --git a/mindplot/src/main/javascript/RestPersistenceManager.js b/mindplot/src/main/javascript/RestPersistenceManager.js index 7335503a..b7d5b2aa 100644 --- a/mindplot/src/main/javascript/RestPersistenceManager.js +++ b/mindplot/src/main/javascript/RestPersistenceManager.js @@ -18,15 +18,17 @@ mindplot.RESTPersistenceManager = new Class({ Extends:mindplot.PersistenceManager, - initialize:function (saveUrl, revertUrl) { + initialize:function (saveUrl, revertUrl, lockUrl) { 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; }, - saveMapXml:function (mapId, mapXml, pref, saveHistory, events) { + saveMapXml:function (mapId, mapXml, pref, saveHistory, events, sync) { var data = { id:mapId, @@ -34,12 +36,17 @@ mindplot.RESTPersistenceManager = new Class({ properties:pref }; + var persistence = this; + var query = "minor=" + !saveHistory; + query = query + (this.timestamp ? "×tamp=" + this.timestamp : ""); + var request = new Request({ - url:this.saveUrl.replace("{id}", mapId) + "?minor=" + !saveHistory, + url:this.saveUrl.replace("{id}", mapId) + "?" + query, method:'put', + async:!sync, onSuccess:function (responseText, responseXML) { events.onSuccess(); - + persistence.timestamp = responseText; }, onException:function (headerName, value) { events.onError(); @@ -81,8 +88,27 @@ mindplot.RESTPersistenceManager = new Class({ urlEncoded:false }); 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"); + } } ); diff --git a/mindplot/src/main/javascript/widget/IMenu.js b/mindplot/src/main/javascript/widget/IMenu.js index 487092d8..0bc89a90 100644 --- a/mindplot/src/main/javascript/widget/IMenu.js +++ b/mindplot/src/main/javascript/widget/IMenu.js @@ -40,7 +40,7 @@ mindplot.widget.IMenu = new Class({ }); }, - discardChanges:function () { + discardChanges:function (designer) { // Avoid autosave before leaving the page .... this.setRequireChange(false); @@ -49,12 +49,21 @@ mindplot.widget.IMenu = new Class({ var mindmap = designer.getMindmap(); persistenceManager.discardChanges(mindmap.getId()); + // Unlock map ... + this.unlockMap(designer); + // Reload the page ... 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 ... var mindmap = designer.getMindmap(); var mindmapProp = designer.getMindmapProperties(); @@ -88,7 +97,8 @@ mindplot.widget.IMenu = new Class({ $notify(msg); } } - }); + }, sync); + }, isSaveRequired:function () { diff --git a/mindplot/src/main/javascript/widget/Menu.js b/mindplot/src/main/javascript/widget/Menu.js index 9a87a477..0432d1de 100644 --- a/mindplot/src/main/javascript/widget/Menu.js +++ b/mindplot/src/main/javascript/widget/Menu.js @@ -325,10 +325,12 @@ mindplot.widget.Menu = new Class({ if (!readOnly) { // 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()) { - this.save(saveElem, designer, false); + this.save(saveElem, designer, false, true); } + this.unlockMap(designer); }.bind(this)); // Autosave on a fixed period of time ... @@ -343,29 +345,11 @@ mindplot.widget.Menu = new Class({ var discardElem = $('discard'); if (discardElem) { this._addButton('discard', false, false, function () { - this.discardChanges(); + this.discardChanges(designer); }.bind(this)); 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'); if (shareElem) { this._addButton('shareIt', false, false, function () { diff --git a/wise-webapp/src/main/java/com/wisemapping/exceptions/LockException.java b/wise-webapp/src/main/java/com/wisemapping/exceptions/LockException.java new file mode 100755 index 00000000..dff6efe0 --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/exceptions/LockException.java @@ -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. + } +} diff --git a/wise-webapp/src/main/java/com/wisemapping/exceptions/UnexpectedArgumentException.java b/wise-webapp/src/main/java/com/wisemapping/exceptions/MindmapOutdatedException.java similarity index 71% rename from wise-webapp/src/main/java/com/wisemapping/exceptions/UnexpectedArgumentException.java rename to wise-webapp/src/main/java/com/wisemapping/exceptions/MindmapOutdatedException.java index 3c346dda..87a65e91 100755 --- a/wise-webapp/src/main/java/com/wisemapping/exceptions/UnexpectedArgumentException.java +++ b/wise-webapp/src/main/java/com/wisemapping/exceptions/MindmapOutdatedException.java @@ -1,29 +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; - - -public class UnexpectedArgumentException - extends Exception -{ - public UnexpectedArgumentException(String msg) - { - super(msg); - } -} +/* +* 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 MindmapOutdatedException + extends ClientException +{ + public static final String MSG_KEY = "MINDMAP_TIMESTAMP_OUTDATED"; + + public MindmapOutdatedException(@NotNull String msg) + { + super(msg); + } + + @NotNull + @Override + protected String getMsgBundleKey() { + return MSG_KEY; + } +} diff --git a/wise-webapp/src/main/java/com/wisemapping/model/Mindmap.java b/wise-webapp/src/main/java/com/wisemapping/model/Mindmap.java index 2dd41384..98a58e7d 100644 --- a/wise-webapp/src/main/java/com/wisemapping/model/Mindmap.java +++ b/wise-webapp/src/main/java/com/wisemapping/model/Mindmap.java @@ -138,10 +138,6 @@ public class Mindmap { return lastModificationTime; } - public Date getLastModificationDate() { - return new Date(); - } - public void setLastModificationTime(Calendar lastModificationTime) { this.lastModificationTime = lastModificationTime; } diff --git a/wise-webapp/src/main/java/com/wisemapping/model/User.java b/wise-webapp/src/main/java/com/wisemapping/model/User.java index f448330f..4f350353 100644 --- a/wise-webapp/src/main/java/com/wisemapping/model/User.java +++ b/wise-webapp/src/main/java/com/wisemapping/model/User.java @@ -20,6 +20,8 @@ package com.wisemapping.model; import org.jetbrains.annotations.Nullable; +import javax.servlet.http.HttpSessionBindingEvent; +import javax.servlet.http.HttpSessionBindingListener; import java.io.Serializable; import java.util.*; diff --git a/wise-webapp/src/main/java/com/wisemapping/ncontroller/MindmapController.java b/wise-webapp/src/main/java/com/wisemapping/ncontroller/MindmapController.java index abbb92bf..b154e0e2 100644 --- a/wise-webapp/src/main/java/com/wisemapping/ncontroller/MindmapController.java +++ b/wise-webapp/src/main/java/com/wisemapping/ncontroller/MindmapController.java @@ -19,12 +19,15 @@ package com.wisemapping.ncontroller; +import com.wisemapping.exceptions.AccessDeniedSecurityException; +import com.wisemapping.exceptions.LockException; import com.wisemapping.exceptions.WiseMappingException; import com.wisemapping.model.CollaborationRole; import com.wisemapping.model.Mindmap; import com.wisemapping.model.MindMapHistory; import com.wisemapping.model.User; import com.wisemapping.security.Utils; +import com.wisemapping.service.LockManager; import com.wisemapping.service.MindmapService; import com.wisemapping.view.MindMapBean; import org.jetbrains.annotations.NotNull; @@ -140,33 +143,47 @@ public class MindmapController { } @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 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); // Configure default locale for the editor ... - final Locale locale = LocaleContextHolder.getLocale(); model.addAttribute("locale", locale.toString().toLowerCase()); - final User collaborator = Utils.getUser(); model.addAttribute("principal", collaborator); - model.addAttribute("readOnlyMode", !mindmap.hasPermissions(collaborator, CollaborationRole.EDITOR)); + model.addAttribute("readOnlyMode", readOnlyMode); return "mindmapEditor"; } @RequestMapping(value = "maps/{id}/view", method = RequestMethod.GET) - public String showMindmapViewerPage(@PathVariable int id, @NotNull Model model) { - final String result = showMindmapEditorPage(id, model); - model.addAttribute("readOnlyMode", true); - return result; + public String showMindmapViewerPage(@PathVariable int id, @NotNull Model model) throws LockException, AccessDeniedSecurityException { + return showEditorPage(id, model, false); } @RequestMapping(value = "maps/{id}/try", method = RequestMethod.GET) - public String showMindmapTryPage(@PathVariable int id, @NotNull Model model) { - final String result = showMindmapEditorPage(id, model); + public String showMindmapTryPage(@PathVariable int id, @NotNull Model model) throws LockException, AccessDeniedSecurityException { + final String result = showEditorPage(id, model, false); model.addAttribute("memoryPersistence", true); - model.addAttribute("readOnlyMode", false); return result; } @@ -213,11 +230,7 @@ public class MindmapController { } private Mindmap findMindmap(long mapId) { - final Mindmap mindmap = mindmapService.findMindmapById((int) mapId); - if (mindmap == null) { - throw new IllegalArgumentException("Mindmap could not be found"); - } - return mindmap; + return mindmapService.findMindmapById((int) mapId); } private MindMapBean findMindmapBean(long mapId) { diff --git a/wise-webapp/src/main/java/com/wisemapping/rest/BaseController.java b/wise-webapp/src/main/java/com/wisemapping/rest/BaseController.java index 232088d6..8b195ca5 100644 --- a/wise-webapp/src/main/java/com/wisemapping/rest/BaseController.java +++ b/wise-webapp/src/main/java/com/wisemapping/rest/BaseController.java @@ -24,6 +24,7 @@ import com.wisemapping.mail.NotificationService; import com.wisemapping.model.User; import com.wisemapping.rest.model.RestErrors; import com.wisemapping.security.Utils; +import org.apache.log4j.Logger; import org.jetbrains.annotations.NotNull; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; @@ -41,6 +42,8 @@ import java.util.Locale; public class BaseController { + final protected static Logger logger = Logger.getLogger("com.wisemapping.rest"); + @Qualifier("messageSource") @Autowired private ResourceBundleMessageSource messageSource; diff --git a/wise-webapp/src/main/java/com/wisemapping/rest/MindmapController.java b/wise-webapp/src/main/java/com/wisemapping/rest/MindmapController.java index ecb4230f..b52a5fec 100644 --- a/wise-webapp/src/main/java/com/wisemapping/rest/MindmapController.java +++ b/wise-webapp/src/main/java/com/wisemapping/rest/MindmapController.java @@ -20,6 +20,7 @@ package com.wisemapping.rest; import com.wisemapping.exceptions.ImportUnexpectedException; +import com.wisemapping.exceptions.MindmapOutdatedException; import com.wisemapping.exceptions.WiseMappingException; import com.wisemapping.importer.ImportFormat; import com.wisemapping.importer.Importer; @@ -29,8 +30,10 @@ import com.wisemapping.model.*; import com.wisemapping.rest.model.*; import com.wisemapping.security.Utils; import com.wisemapping.service.CollaborationException; +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; @@ -49,6 +52,7 @@ import java.util.*; @Controller public class MindmapController extends BaseController { + public static final String LATEST_HISTORY_REVISION = "latest"; @Qualifier("mindmapService") @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"}) - @ResponseStatus(value = HttpStatus.NO_CONTENT) - public void updateDocument(@RequestBody RestMindmap restMindmap, @PathVariable int id, @RequestParam(required = false) boolean minor) throws WiseMappingException, IOException { + @ResponseBody + 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 User user = Utils.getUser(); @@ -148,6 +152,11 @@ 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); + } + // Update collaboration properties ... final CollaborationProperties collaborationProperties = mindmap.findCollaborationProperties(user); collaborationProperties.setMindmapProperties(properties); @@ -160,7 +169,11 @@ public class MindmapController extends BaseController { mindmap.setXmlStr(xml); // Update map ... + logger.debug("Mindmap save completed:" + restMindmap.getXml()); 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"}) @ResponseStatus(value = HttpStatus.NO_CONTENT) public void updateStarredState(@RequestBody String value, @PathVariable int id) throws WiseMappingException { @@ -334,12 +355,13 @@ public class MindmapController extends BaseController { 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) - 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 LockManager lockManager = mindmapService.getLockManager(); final Mindmap mindmap = mindmapService.findMindmapById(id); - mindmapService.removeMindmap(mindmap, user); + lockManager.updateLock(Boolean.parseBoolean(value), mindmap, user); } @RequestMapping(method = RequestMethod.DELETE, value = "/maps/batch") diff --git a/wise-webapp/src/main/java/com/wisemapping/rest/model/RestMindmapLock.java b/wise-webapp/src/main/java/com/wisemapping/rest/model/RestMindmapLock.java new file mode 100644 index 00000000..5377b215 --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/rest/model/RestMindmapLock.java @@ -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 ... + } +} diff --git a/wise-webapp/src/main/java/com/wisemapping/security/AuthenticationProvider.java b/wise-webapp/src/main/java/com/wisemapping/security/AuthenticationProvider.java index 9d0ce501..04ef95b3 100644 --- a/wise-webapp/src/main/java/com/wisemapping/security/AuthenticationProvider.java +++ b/wise-webapp/src/main/java/com/wisemapping/security/AuthenticationProvider.java @@ -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; diff --git a/wise-webapp/src/main/java/com/wisemapping/security/aop/BaseSecurityAdvice.java b/wise-webapp/src/main/java/com/wisemapping/security/aop/BaseSecurityAdvice.java index 4f4bea73..2a6da5a8 100755 --- a/wise-webapp/src/main/java/com/wisemapping/security/aop/BaseSecurityAdvice.java +++ b/wise-webapp/src/main/java/com/wisemapping/security/aop/BaseSecurityAdvice.java @@ -22,7 +22,6 @@ import com.wisemapping.model.Collaborator; import com.wisemapping.model.Mindmap; import com.wisemapping.model.User; import com.wisemapping.exceptions.AccessDeniedSecurityException; -import com.wisemapping.exceptions.UnexpectedArgumentException; import com.wisemapping.security.Utils; import com.wisemapping.service.MindmapService; import org.aopalliance.intercept.MethodInvocation; @@ -31,7 +30,7 @@ import org.jetbrains.annotations.Nullable; public abstract class BaseSecurityAdvice { 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 Object argument = methodInvocation.getArguments()[0]; boolean isAllowed; @@ -44,7 +43,7 @@ public abstract class BaseSecurityAdvice { // Read operation find on the user are allowed ... isAllowed = user.equals(argument); } else { - throw new UnexpectedArgumentException("Argument " + argument); + throw new IllegalArgumentException("Argument " + argument); } if (!isAllowed) { diff --git a/wise-webapp/src/main/java/com/wisemapping/service/LockInfo.java b/wise-webapp/src/main/java/com/wisemapping/service/LockInfo.java new file mode 100644 index 00000000..d45be415 --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/service/LockInfo.java @@ -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; + + } +} diff --git a/wise-webapp/src/main/java/com/wisemapping/service/LockManager.java b/wise-webapp/src/main/java/com/wisemapping/service/LockManager.java new file mode 100644 index 00000000..9a71facc --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/service/LockManager.java @@ -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; +} diff --git a/wise-webapp/src/main/java/com/wisemapping/service/LockManagerImpl.java b/wise-webapp/src/main/java/com/wisemapping/service/LockManagerImpl.java new file mode 100644 index 00000000..a886568b --- /dev/null +++ b/wise-webapp/src/main/java/com/wisemapping/service/LockManagerImpl.java @@ -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 lockInfoByMapId; + final static Timer expirationTimer = new Timer(); + final private static Logger logger = Logger.getLogger("com.wisemapping.service.LockManager"); + + public LockManagerImpl() { + lockInfoByMapId = new ConcurrentHashMap(); + expirationTimer.schedule(new TimerTask() { + @Override + public void run() { + + logger.debug("Lock expiration scheduler started. Current locks:" + lockInfoByMapId.keySet()); + + final List toRemove = new ArrayList(); + final Set 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); + } + } +} diff --git a/wise-webapp/src/main/java/com/wisemapping/service/MindmapService.java b/wise-webapp/src/main/java/com/wisemapping/service/MindmapService.java index 76c8cbb7..937fd1a3 100755 --- a/wise-webapp/src/main/java/com/wisemapping/service/MindmapService.java +++ b/wise-webapp/src/main/java/com/wisemapping/service/MindmapService.java @@ -62,4 +62,6 @@ public interface MindmapService { MindMapHistory findMindmapHistory(int id, int hid) throws WiseMappingException; void updateCollaboration(@NotNull Collaborator collaborator, @NotNull Collaboration collaboration) throws WiseMappingException; + + LockManager getLockManager(); } diff --git a/wise-webapp/src/main/java/com/wisemapping/service/MindmapServiceImpl.java b/wise-webapp/src/main/java/com/wisemapping/service/MindmapServiceImpl.java index 92b754b1..941bff53 100755 --- a/wise-webapp/src/main/java/com/wisemapping/service/MindmapServiceImpl.java +++ b/wise-webapp/src/main/java/com/wisemapping/service/MindmapServiceImpl.java @@ -45,6 +45,11 @@ public class MindmapServiceImpl private NotificationService notificationService; private String adminUser; + final private LockManager lockManager; + + public MindmapServiceImpl() { + this.lockManager = new LockManagerImpl(); + } @Override 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) { 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); } @@ -264,6 +274,12 @@ public class MindmapServiceImpl mindmapManager.updateCollaboration(collaboration); } + @Override + @NotNull + public LockManager getLockManager() { + return this.lockManager; + } + private Collaboration getCollaborationBy(String email, Set collaborations) { Collaboration collaboration = null; diff --git a/wise-webapp/src/main/resources/messages_en.properties b/wise-webapp/src/main/resources/messages_en.properties index e67d4c18..9097bd5f 100644 --- a/wise-webapp/src/main/resources/messages_en.properties +++ b/wise-webapp/src/main/resources/messages_en.properties @@ -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_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 support@wisemapping.com - +MINDMAP_TIMESTAMP_OUTDATED=It's not possible to save your changes because your mindmap is out of date. Refresh the page and try again. diff --git a/wise-webapp/src/main/webapp/WEB-INF/classes/log4j.properties b/wise-webapp/src/main/webapp/WEB-INF/classes/log4j.properties index fc70ce71..5f160b6e 100644 --- a/wise-webapp/src/main/webapp/WEB-INF/classes/log4j.properties +++ b/wise-webapp/src/main/webapp/WEB-INF/classes/log4j.properties @@ -1,8 +1,9 @@ log4j.rootLogger=WARN, stdout, R +log4j.logger.com.wisemapping.service.LockManager=DEBUG,stdout,R log4j.logger.com.wisemapping=WARN,stdout,R log4j.logger.org.springframework=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 diff --git a/wise-webapp/src/main/webapp/jsp/mindmapEditor.jsp b/wise-webapp/src/main/webapp/jsp/mindmapEditor.jsp index 5c857efa..cf46b517 100644 --- a/wise-webapp/src/main/webapp/jsp/mindmapEditor.jsp +++ b/wise-webapp/src/main/webapp/jsp/mindmapEditor.jsp @@ -35,7 +35,7 @@ // Configure designer options ... var options = loadDesignerOptions(); - 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"); var userOptions = ${mindmap.properties}; options.zoom = userOptions.zoom; diff --git a/wise-webapp/src/test/resources/data/freemind/node-styles.mmr b/wise-webapp/src/test/resources/data/freemind/node-styles.mmr index a643efe8..b36596f0 100644 --- a/wise-webapp/src/test/resources/data/freemind/node-styles.mmr +++ b/wise-webapp/src/test/resources/data/freemind/node-styles.mmr @@ -43,4 +43,4 @@ - \ No newline at end of file +