From 164a164287f7290cb03cfe6e06e6ba25d429f67a Mon Sep 17 00:00:00 2001
From: Piotr Gawron <piotr.gawron@uni.lu>
Date: Wed, 7 Jun 2017 16:20:24 +0200
Subject: [PATCH] new api calls for handling comments

---
 frontend-js/src/main/js/ServerConnector.js    |  79 +++++----
 frontend-js/src/main/js/gui/CommentDialog.js  |   8 +-
 .../src/test/js/ServerConnector-mock.js       |   2 +-
 frontend-js/src/test/js/map/CustomMap-test.js |  19 ++
 ...alse&projectId=sample&token=MOCK_TOKEN_ID& |   1 -
 ...%2C12.00&pinned=false&token=MOCK_TOKEN_ID& |   1 +
 .../POST_pinned=false&token=MOCK_TOKEN_ID&    |   1 +
 .../elements/3001/token=MOCK_TOKEN_ID&}       |   0
 .../elements/329158/token=MOCK_TOKEN_ID&}     |   0
 ...Id,type,icon,removed&token=MOCK_TOKEN_ID&} |   0
 .../api/comment/CommentController.java        |  73 --------
 .../projects/comments/CommentController.java  | 162 ++++++++++++++++++
 .../comments}/CommentRestImpl.java            |  12 +-
 .../resources/applicationContext-rest.xml     |   3 +-
 .../api/comment/CommentRestImplTest.java      |   1 +
 15 files changed, 244 insertions(+), 118 deletions(-)
 delete mode 100644 frontend-js/testFiles/apiCalls/comment/addComment/POST_coordinates=2.00%2C12.00&elementType=POINT&modelId=15781&pinned=false&projectId=sample&token=MOCK_TOKEN_ID&
 create mode 100644 frontend-js/testFiles/apiCalls/projects/sample/comments/models/15781/bioEntities/elements/329173/POST_coordinates=2.00%2C12.00&pinned=false&token=MOCK_TOKEN_ID&
 create mode 100644 frontend-js/testFiles/apiCalls/projects/sample/comments/models/15781/points/2.00,12.00/POST_pinned=false&token=MOCK_TOKEN_ID&
 rename frontend-js/testFiles/apiCalls/{comment/getCommentList/elementId=3001&elementType=ALIAS&projectId=sample&token=MOCK_TOKEN_ID& => projects/sample/comments/models/all/bioEntities/elements/3001/token=MOCK_TOKEN_ID&} (100%)
 rename frontend-js/testFiles/apiCalls/{comment/getCommentList/elementId=329158&elementType=ALIAS&projectId=sample&token=MOCK_TOKEN_ID& => projects/sample/comments/models/all/bioEntities/elements/329158/token=MOCK_TOKEN_ID&} (100%)
 rename frontend-js/testFiles/apiCalls/{comment/getCommentList/columns=id,elementId,modelId,type,icon,removed&projectId=sample&token=MOCK_TOKEN_ID& => projects/sample/comments/models/all/columns=id,elementId,modelId,type,icon,removed&token=MOCK_TOKEN_ID&} (100%)
 delete mode 100644 rest-api/src/main/java/lcsb/mapviewer/api/comment/CommentController.java
 create mode 100644 rest-api/src/main/java/lcsb/mapviewer/api/projects/comments/CommentController.java
 rename rest-api/src/main/java/lcsb/mapviewer/api/{comment => projects/comments}/CommentRestImpl.java (92%)

diff --git a/frontend-js/src/main/js/ServerConnector.js b/frontend-js/src/main/js/ServerConnector.js
index cbd702092b..ff50af871d 100644
--- a/frontend-js/src/main/js/ServerConnector.js
+++ b/frontend-js/src/main/js/ServerConnector.js
@@ -309,10 +309,10 @@ ServerConnector.getSuggestedQueryListUrl = function(queryParams, filterParams) {
   });
 };
 
-ServerConnector.addCommentUrl = function() {
+ServerConnector.addCommentUrl = function(queryParams) {
+
   return this.getApiUrl({
-    type : "comment",
-    method : "addComment",
+    url : this.getCommentsUrl(queryParams)
   });
 };
 
@@ -341,23 +341,23 @@ ServerConnector.getOverlaysUrl = function(queryParams, filterParams) {
   });
 };
 
-ServerConnector.getCommentsUrl = function(params) {
-  var elementId = params.elementId;
-  var elementType = params.elementType;
-  var columns = this.columnsToString(params.columns);
-  var projectId = params.projectId;
-  var token = params.token;
-
+ServerConnector.getCommentsUrl = function(queryParams, filterParams) {
+  var modelId = this.getIdOrAsterisk(queryParams.modelId);
+  var url = this.getProjectsUrl(queryParams) + "comments/models/" + modelId + "/";
+  if (queryParams.elementType !== undefined) {
+    if (queryParams.elementType === "ALIAS") {
+      url += "bioEntities/elements/" + queryParams.elementId;
+    } else if (queryParams.elementType === "REACTION") {
+      url += "bioEntities/reactions/" + queryParams.elementId;
+    } else if (queryParams.elementType === "POINT") {
+      url += "points/" + queryParams.coordinates;
+    } else {
+      throw new Error("Unknown element type: " + queryParams.elementType);
+    }
+  }
   return this.getApiUrl({
-    type : "comment",
-    method : "getCommentList",
-    params : {
-      projectId : projectId,
-      columns : columns,
-      elementId : elementId,
-      elementType : elementType,
-      token : token
-    },
+    url : url,
+    params : filterParams,
   });
 };
 
@@ -856,12 +856,19 @@ ServerConnector.getLightComments = function(params) {
 
 ServerConnector.getComments = function(params) {
   var self = this;
+  var queryParams = {
+    elementId : params.elementId,
+    elementType : params.elementType,
+  };
+  var filterParams = {
+    columns : params.columns
+  };
   return self.getProjectId(params.projectId).then(function(result) {
-    params.projectId = result;
+    queryParams.projectId = result;
     return self.getToken();
   }).then(function(token) {
-    params.token = token;
-    return self.readFile(self.getCommentsUrl(params));
+    filterParams.token = token;
+    return self.readFile(self.getCommentsUrl(queryParams, filterParams));
   }).then(function(content) {
     var array = JSON.parse(content);
     var result = [];
@@ -1179,20 +1186,30 @@ ServerConnector.getChemicalNamesByTarget = function(params) {
 
 ServerConnector.addComment = function(params) {
   var self = this;
+  var queryParams = {
+    elementId : params.elementId,
+    elementType : params.elementType,
+    coordinates : self.pointToString(params.coordinates),
+    modelId : params.modelId,
+  };
+  var filterParams = params;
+  delete filterParams.elementId;
+  delete filterParams.elementType;
+  if (queryParams.elementType == "POINT") {
+    delete filterParams.coordinates;
+  } else {
+    filterParams.coordinates = self.pointToString(params.coordinates);
+  }
+  delete filterParams.modelId;
   return self.getProjectId(params.projectId).then(function(result) {
-    params.projectId = result;
+    queryParams.projectId = result;
     return self.getToken();
   }).then(function(token) {
-    params.token = token;
-    params.coordinates = self.pointToString(params.coordinates);
-    return self.sendPostRequest(self.addCommentUrl(), params);
+    filterParams.token = token;
+    return self.sendPostRequest(self.addCommentUrl(queryParams), filterParams);
   }).then(function(content) {
     var response = JSON.parse(content);
-    if (response.status === "OK") {
-      return Promise.resolve();
-    } else {
-      return Promise.reject(response);
-    }
+    return new Comment(response);
   });
 };
 
diff --git a/frontend-js/src/main/js/gui/CommentDialog.js b/frontend-js/src/main/js/gui/CommentDialog.js
index c1391876f6..ee7624c82d 100644
--- a/frontend-js/src/main/js/gui/CommentDialog.js
+++ b/frontend-js/src/main/js/gui/CommentDialog.js
@@ -108,12 +108,8 @@ CommentDialog.prototype._createGui = function() {
   var sendButton = document.createElement('button');
   sendButton.innerHTML = "Send";
   sendButton.onclick = function() {
-    self.addComment().then(function() {
-      if (self.close !== undefined) {
-        self.close();
-      } else {
-        logger.warn("Cannot close dialog");
-      }
+    return self.addComment().then(function() {
+      $(self.getElement()).dialog("close");
     });
   };
 
diff --git a/frontend-js/src/test/js/ServerConnector-mock.js b/frontend-js/src/test/js/ServerConnector-mock.js
index 1d42c71abe..bafa93713d 100644
--- a/frontend-js/src/test/js/ServerConnector-mock.js
+++ b/frontend-js/src/test/js/ServerConnector-mock.js
@@ -78,7 +78,7 @@ ServerConnectorMock.sendPostRequest = function(url, params) {
         }
       });
     } else {
-      var mockUrl = url + "/POST_" + self.createGetParams(encodeParams(params));
+      var mockUrl = replaceAsterisk(url + "/POST_" + self.createGetParams(encodeParams(params)));
       fs.readFile(mockUrl, 'utf8', function(err, content) {
         if (err) {
           reject(err);
diff --git a/frontend-js/src/test/js/map/CustomMap-test.js b/frontend-js/src/test/js/map/CustomMap-test.js
index ebbb803832..ccc65007fc 100644
--- a/frontend-js/src/test/js/map/CustomMap-test.js
+++ b/frontend-js/src/test/js/map/CustomMap-test.js
@@ -703,6 +703,25 @@ describe('CustomMap', function() {
     });
   });
 
+  it("addComment for protein", function() {
+    var map;
+    var dialog;
+    var alias; 
+    return ServerConnector.getProject().then(function(project) {
+      var options = helper.createCustomMapOptions(project);
+      map = new CustomMap(options);
+      map.setActiveSubmapId(map.getId());
+      map.setActiveSubmapClickCoordinates(new google.maps.Point(2, 12));
+      return map.openCommentDialog();
+    }).then(function() {
+      dialog = map.getCommentDialog();
+      dialog.setSelectedType(1);
+      return dialog.addComment();
+    }).then(function() {
+      map.getCommentDialog().destroy();
+    });
+  });
+
   it("retrieveOverlayDetailDataForElement for comment", function() {
     var map = helper.createCustomMap();
     helper.createCommentDbOverlay(map);
diff --git a/frontend-js/testFiles/apiCalls/comment/addComment/POST_coordinates=2.00%2C12.00&elementType=POINT&modelId=15781&pinned=false&projectId=sample&token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/comment/addComment/POST_coordinates=2.00%2C12.00&elementType=POINT&modelId=15781&pinned=false&projectId=sample&token=MOCK_TOKEN_ID&
deleted file mode 100644
index 48aa9beb26..0000000000
--- a/frontend-js/testFiles/apiCalls/comment/addComment/POST_coordinates=2.00%2C12.00&elementType=POINT&modelId=15781&pinned=false&projectId=sample&token=MOCK_TOKEN_ID&
+++ /dev/null
@@ -1 +0,0 @@
-{"status":"OK"}
\ No newline at end of file
diff --git a/frontend-js/testFiles/apiCalls/projects/sample/comments/models/15781/bioEntities/elements/329173/POST_coordinates=2.00%2C12.00&pinned=false&token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/projects/sample/comments/models/15781/bioEntities/elements/329173/POST_coordinates=2.00%2C12.00&pinned=false&token=MOCK_TOKEN_ID&
new file mode 100644
index 0000000000..abf1d949b0
--- /dev/null
+++ b/frontend-js/testFiles/apiCalls/projects/sample/comments/models/15781/bioEntities/elements/329173/POST_coordinates=2.00%2C12.00&pinned=false&token=MOCK_TOKEN_ID&
@@ -0,0 +1 @@
+{"elementId":329173,"coord":{"x":735.0,"y":259.0},"removed":false,"modelId":15781,"icon":"icons/comment.png","id":4673,"title":"s23","type":"ALIAS","content":""}
\ No newline at end of file
diff --git a/frontend-js/testFiles/apiCalls/projects/sample/comments/models/15781/points/2.00,12.00/POST_pinned=false&token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/projects/sample/comments/models/15781/points/2.00,12.00/POST_pinned=false&token=MOCK_TOKEN_ID&
new file mode 100644
index 0000000000..0304edbc07
--- /dev/null
+++ b/frontend-js/testFiles/apiCalls/projects/sample/comments/models/15781/points/2.00,12.00/POST_pinned=false&token=MOCK_TOKEN_ID&
@@ -0,0 +1 @@
+{"elementId":"(216.65,370.00)","coord":{"x":216.65,"y":370.0},"removed":false,"modelId":15781,"icon":"icons/comment.png","id":4671,"title":"Comment (coord: 2.00,12.00)","type":"POINT","content":""}
\ No newline at end of file
diff --git a/frontend-js/testFiles/apiCalls/comment/getCommentList/elementId=3001&elementType=ALIAS&projectId=sample&token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/projects/sample/comments/models/all/bioEntities/elements/3001/token=MOCK_TOKEN_ID&
similarity index 100%
rename from frontend-js/testFiles/apiCalls/comment/getCommentList/elementId=3001&elementType=ALIAS&projectId=sample&token=MOCK_TOKEN_ID&
rename to frontend-js/testFiles/apiCalls/projects/sample/comments/models/all/bioEntities/elements/3001/token=MOCK_TOKEN_ID&
diff --git a/frontend-js/testFiles/apiCalls/comment/getCommentList/elementId=329158&elementType=ALIAS&projectId=sample&token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/projects/sample/comments/models/all/bioEntities/elements/329158/token=MOCK_TOKEN_ID&
similarity index 100%
rename from frontend-js/testFiles/apiCalls/comment/getCommentList/elementId=329158&elementType=ALIAS&projectId=sample&token=MOCK_TOKEN_ID&
rename to frontend-js/testFiles/apiCalls/projects/sample/comments/models/all/bioEntities/elements/329158/token=MOCK_TOKEN_ID&
diff --git a/frontend-js/testFiles/apiCalls/comment/getCommentList/columns=id,elementId,modelId,type,icon,removed&projectId=sample&token=MOCK_TOKEN_ID& b/frontend-js/testFiles/apiCalls/projects/sample/comments/models/all/columns=id,elementId,modelId,type,icon,removed&token=MOCK_TOKEN_ID&
similarity index 100%
rename from frontend-js/testFiles/apiCalls/comment/getCommentList/columns=id,elementId,modelId,type,icon,removed&projectId=sample&token=MOCK_TOKEN_ID&
rename to frontend-js/testFiles/apiCalls/projects/sample/comments/models/all/columns=id,elementId,modelId,type,icon,removed&token=MOCK_TOKEN_ID&
diff --git a/rest-api/src/main/java/lcsb/mapviewer/api/comment/CommentController.java b/rest-api/src/main/java/lcsb/mapviewer/api/comment/CommentController.java
deleted file mode 100644
index 6d2a17042b..0000000000
--- a/rest-api/src/main/java/lcsb/mapviewer/api/comment/CommentController.java
+++ /dev/null
@@ -1,73 +0,0 @@
-package lcsb.mapviewer.api.comment;
-
-import java.awt.geom.Point2D;
-import java.util.List;
-import java.util.Map;
-
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.http.MediaType;
-import org.springframework.web.bind.annotation.RequestMapping;
-import org.springframework.web.bind.annotation.RequestMethod;
-import org.springframework.web.bind.annotation.RequestParam;
-import org.springframework.web.bind.annotation.RestController;
-
-import lcsb.mapviewer.api.BaseController;
-import lcsb.mapviewer.api.QueryException;
-import lcsb.mapviewer.services.SecurityException;
-
-@RestController
-@RequestMapping("/comment")
-public class CommentController extends BaseController {
-
-	@Autowired
-	private CommentRestImpl commentController;
-
-	@RequestMapping(value = "/getCommentList", method = { RequestMethod.GET, RequestMethod.POST }, produces = { MediaType.APPLICATION_JSON_VALUE })
-	public List<Map<String, Object>> getOverlayList(@RequestParam(value = "token") String token, @RequestParam(value = "projectId") String projectId,
-			@RequestParam(value = "columns", defaultValue = "") String columns, @RequestParam(value = "elementId", defaultValue = "") String elementId,
-			@RequestParam(value = "elementType", defaultValue = "") String elementType, @RequestParam(value = "removed", defaultValue = "") String removed)
-			throws SecurityException, QueryException {
-		return commentController.getCommentList(token, projectId, columns, elementId, elementType, removed);
-	}
-
-	@RequestMapping(value = "/addComment", method = { RequestMethod.GET, RequestMethod.POST }, produces = { MediaType.APPLICATION_JSON_VALUE })
-	public Map<String, Object> addComment(@RequestParam(value = "token") String token, @RequestParam(value = "projectId") String projectId,
-			@RequestParam(value = "elementType", defaultValue = "POINT") String elementType, @RequestParam(value = "elementId", defaultValue = "") String elementId,
-			@RequestParam(value = "name") String name, @RequestParam(value = "email") String email, @RequestParam(value = "content") String content,
-			@RequestParam(value = "pinned", defaultValue = "true") String pinned, @RequestParam(value = "coordinates") String coordinates,
-			@RequestParam(value = "modelId") String modelId) throws SecurityException, QueryException {
-		String[] tmp = coordinates.split(",");
-		if (tmp.length != 2) {
-			throw new QueryException("Coordinates must be in the format: 'xxx.xx,yyy.yy'");
-		}
-		Double x = null;
-		Double y = null;
-		try {
-			x = Double.valueOf(tmp[0]);
-			y = Double.valueOf(tmp[1]);
-		} catch (NumberFormatException e) {
-			throw new QueryException("Coordinates must be in the format: 'xxx.xx,yyy.yy'", e);
-		}
-		Point2D pointCoordinates = new Point2D.Double(x, y);
-		return commentController
-				.addComment(token, projectId, elementType, elementId, name, email, content, pinned.toLowerCase().equals("true"), pointCoordinates, modelId);
-	}
-
-	/**
-	 * @return the overlayController
-	 * @see #commentController
-	 */
-	public CommentRestImpl getOverlayController() {
-		return commentController;
-	}
-
-	/**
-	 * @param overlayController
-	 *          the overlayController to set
-	 * @see #commentController
-	 */
-	public void setOverlayController(CommentRestImpl overlayController) {
-		this.commentController = overlayController;
-	}
-
-}
\ No newline at end of file
diff --git a/rest-api/src/main/java/lcsb/mapviewer/api/projects/comments/CommentController.java b/rest-api/src/main/java/lcsb/mapviewer/api/projects/comments/CommentController.java
new file mode 100644
index 0000000000..f90116d329
--- /dev/null
+++ b/rest-api/src/main/java/lcsb/mapviewer/api/projects/comments/CommentController.java
@@ -0,0 +1,162 @@
+package lcsb.mapviewer.api.projects.comments;
+
+import java.awt.geom.Point2D;
+import java.util.List;
+import java.util.Map;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import lcsb.mapviewer.api.BaseController;
+import lcsb.mapviewer.api.QueryException;
+import lcsb.mapviewer.services.SecurityException;
+import lcsb.mapviewer.services.search.data.ElementIdentifier.ElementIdentifierType;
+
+@RestController
+public class CommentController extends BaseController {
+
+	@Autowired
+	private CommentRestImpl commentController;
+
+	@RequestMapping(value = "/projects/{projectId}/comments/models/{modelId}/", method = { RequestMethod.GET }, produces = { MediaType.APPLICATION_JSON_VALUE })
+	public List<Map<String, Object>> getComments(//
+			@RequestParam(value = "token") String token, //
+			@PathVariable(value = "projectId") String projectId, //
+			@RequestParam(value = "columns", defaultValue = "") String columns, //
+			@RequestParam(value = "removed", defaultValue = "") String removed //
+	) throws SecurityException, QueryException {
+		return commentController.getCommentList(token, projectId, columns, "", "", removed);
+	}
+
+	@RequestMapping(value = "/projects/{projectId}/comments/models/{modelId}/bioEntities/reactions/{reactionId}", method = { RequestMethod.GET },
+			produces = { MediaType.APPLICATION_JSON_VALUE })
+	public List<Map<String, Object>> getCommentsByReaction(//
+			@RequestParam(value = "token") String token, //
+			@PathVariable(value = "projectId") String projectId, //
+			@RequestParam(value = "columns", defaultValue = "") String columns, //
+			@PathVariable(value = "reactionId") String reactionId, //
+			@RequestParam(value = "removed", defaultValue = "") String removed //
+	) throws SecurityException, QueryException {
+		return commentController.getCommentList(token, projectId, columns, reactionId, ElementIdentifierType.REACTION.getJsName(), removed);
+	}
+
+	@RequestMapping(value = "/projects/{projectId}/comments/models/{modelId}/bioEntities/elements/{elementId}", method = { RequestMethod.GET },
+			produces = { MediaType.APPLICATION_JSON_VALUE })
+	public List<Map<String, Object>> getCommentsByElement(//
+			@RequestParam(value = "token") String token, //
+			@PathVariable(value = "projectId") String projectId, //
+			@RequestParam(value = "columns", defaultValue = "") String columns, //
+			@PathVariable(value = "elementId") String elementId, //
+			@RequestParam(value = "removed", defaultValue = "") String removed //
+	) throws SecurityException, QueryException {
+		return commentController.getCommentList(token, projectId, columns, elementId, ElementIdentifierType.ALIAS.getJsName(), removed);
+	}
+
+	@RequestMapping(value = "/projects/{projectId}/comments/models/{modelId}/points/{coordinates}", method = { RequestMethod.GET },
+			produces = { MediaType.APPLICATION_JSON_VALUE })
+	public List<Map<String, Object>> getCommentsByPoint(//
+			@RequestParam(value = "token") String token, //
+			@PathVariable(value = "projectId") String projectId, //
+			@RequestParam(value = "columns", defaultValue = "") String columns, //
+			@PathVariable(value = "coordinates") String coordinates, //
+			@RequestParam(value = "removed", defaultValue = "") String removed //
+	) throws SecurityException, QueryException {
+		return commentController.getCommentList(token, projectId, columns, coordinates, ElementIdentifierType.POINT.getJsName(), removed);
+	}
+
+	@RequestMapping(value = "/projects/{projectId}/comments/models/{modelId}/bioEntities/elements/{elementId}", method = { RequestMethod.POST },
+			produces = { MediaType.APPLICATION_JSON_VALUE })
+	public Map<String, Object> addCommentForElement(//
+			@RequestParam(value = "token") String token, //
+			@PathVariable(value = "projectId") String projectId, // 
+			@PathVariable(value = "elementId") String elementId, //
+			@RequestParam(value = "name") String name, //
+			@RequestParam(value = "email") String email, //
+			@RequestParam(value = "content") String content, //
+			@RequestParam(value = "pinned", defaultValue = "true") String pinned, //
+			@RequestParam(value = "coordinates") String coordinates, //
+			@PathVariable(value = "modelId") String modelId //
+	) throws SecurityException, QueryException {
+		Point2D pointCoordinates = parseCoordinates(coordinates);
+		return commentController.addComment(
+				token, projectId, ElementIdentifierType.ALIAS.getJsName(), elementId, name, email, content, pinned.toLowerCase().equals("true"), pointCoordinates,
+				modelId);
+	}
+
+	@RequestMapping(value = "/projects/{projectId}/comments/models/{modelId}/bioEntities/reactions/{reactionId}", method = { RequestMethod.POST },
+			produces = { MediaType.APPLICATION_JSON_VALUE })
+	public Map<String, Object> addCommentForReaction(//
+			@RequestParam(value = "token") String token, //
+			@PathVariable(value = "projectId") String projectId, // 
+			@PathVariable(value = "reactionId") String reactionId, //
+			@RequestParam(value = "name") String name, //
+			@RequestParam(value = "email") String email, //
+			@RequestParam(value = "content") String content, //
+			@RequestParam(value = "pinned", defaultValue = "true") String pinned, //
+			@RequestParam(value = "coordinates") String coordinates, //
+			@PathVariable(value = "modelId") String modelId //
+	) throws SecurityException, QueryException {
+		Point2D pointCoordinates = parseCoordinates(coordinates);
+		return commentController.addComment(
+				token, projectId, ElementIdentifierType.REACTION.getJsName(), reactionId, name, email, content, pinned.toLowerCase().equals("true"), pointCoordinates,
+				modelId);
+	}
+
+	@RequestMapping(value = "/projects/{projectId}/comments/models/{modelId}/points/{coordinates}", method = { RequestMethod.POST },
+			produces = { MediaType.APPLICATION_JSON_VALUE })
+	public Map<String, Object> addCommentForPoint(//
+			@RequestParam(value = "token") String token, //
+			@PathVariable(value = "projectId") String projectId, // 
+			@RequestParam(value = "name") String name, //
+			@RequestParam(value = "email") String email, //
+			@RequestParam(value = "content") String content, //
+			@RequestParam(value = "pinned", defaultValue = "true") String pinned, //
+			@PathVariable(value = "coordinates") String coordinates, //
+			@PathVariable(value = "modelId") String modelId //
+	) throws SecurityException, QueryException {
+		Point2D pointCoordinates = parseCoordinates(coordinates);
+		return commentController.addComment(
+				token, projectId, ElementIdentifierType.POINT.getJsName(), coordinates, name, email, content, pinned.toLowerCase().equals("true"), pointCoordinates,
+				modelId);
+	}
+
+	private Point2D parseCoordinates(String coordinates) throws QueryException {
+		String[] tmp = coordinates.split(",");
+		if (tmp.length != 2) {
+			throw new QueryException("Coordinates must be in the format: 'xxx.xx,yyy.yy'");
+		}
+		Double x = null;
+		Double y = null;
+		try {
+			x = Double.valueOf(tmp[0]);
+			y = Double.valueOf(tmp[1]);
+		} catch (NumberFormatException e) {
+			throw new QueryException("Coordinates must be in the format: 'xxx.xx,yyy.yy'", e);
+		}
+		Point2D pointCoordinates = new Point2D.Double(x, y);
+		return pointCoordinates;
+	}
+
+	/**
+	 * @return the overlayController
+	 * @see #commentController
+	 */
+	public CommentRestImpl getOverlayController() {
+		return commentController;
+	}
+
+	/**
+	 * @param overlayController
+	 *          the overlayController to set
+	 * @see #commentController
+	 */
+	public void setOverlayController(CommentRestImpl overlayController) {
+		this.commentController = overlayController;
+	}
+
+}
\ No newline at end of file
diff --git a/rest-api/src/main/java/lcsb/mapviewer/api/comment/CommentRestImpl.java b/rest-api/src/main/java/lcsb/mapviewer/api/projects/comments/CommentRestImpl.java
similarity index 92%
rename from rest-api/src/main/java/lcsb/mapviewer/api/comment/CommentRestImpl.java
rename to rest-api/src/main/java/lcsb/mapviewer/api/projects/comments/CommentRestImpl.java
index ad9ae26a8b..f4fbd153e1 100644
--- a/rest-api/src/main/java/lcsb/mapviewer/api/comment/CommentRestImpl.java
+++ b/rest-api/src/main/java/lcsb/mapviewer/api/projects/comments/CommentRestImpl.java
@@ -1,4 +1,4 @@
-package lcsb.mapviewer.api.comment;
+package lcsb.mapviewer.api.projects.comments;
 
 import java.awt.geom.Point2D;
 import java.util.ArrayList;
@@ -13,6 +13,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.transaction.annotation.Transactional;
 
 import lcsb.mapviewer.api.BaseRestImpl;
+import lcsb.mapviewer.api.ObjectNotFoundException;
 import lcsb.mapviewer.api.QueryException;
 import lcsb.mapviewer.model.Project;
 import lcsb.mapviewer.model.map.Comment;
@@ -325,7 +326,7 @@ public class CommentRestImpl extends BaseRestImpl {
 		AuthenticationToken authenticationToken = userService.getToken(token);
 		Model model = modelService.getLastModelByProjectId(projectId, authenticationToken);
 		if (model == null) {
-			throw new QueryException("Project with given id doesn't exist");
+			throw new ObjectNotFoundException("Project with given id doesn't exist");
 		}
 
 		Integer modelId = null;
@@ -355,8 +356,11 @@ public class CommentRestImpl extends BaseRestImpl {
 			throw new QueryException("Unknown type of commented object: " + elementType);
 		}
 
-		commentService.addComment(name, email, content, model, pointCoordinates, commentedObject, pinned, submodel);
-		return okStatus();
+		Comment comment = commentService.addComment(name, email, content, model, pointCoordinates, commentedObject, pinned, submodel);
+
+		Project project = model.getProject();
+		boolean isAdmin = userService.userHasPrivilege(authenticationToken, PrivilegeType.EDIT_COMMENTS_PROJECT, project);
+		return preparedComment(comment, createCommentColumnSet("", isAdmin), isAdmin);
 	}
 
 }
diff --git a/rest-api/src/main/resources/applicationContext-rest.xml b/rest-api/src/main/resources/applicationContext-rest.xml
index c78c9b3939..6129a52e82 100644
--- a/rest-api/src/main/resources/applicationContext-rest.xml
+++ b/rest-api/src/main/resources/applicationContext-rest.xml
@@ -13,8 +13,7 @@
 	
 	<bean id="ConfigurationRestImpl" class="lcsb.mapviewer.api.configuration.ConfigurationRestImpl"/>
 
-	<bean id="CommentRestImpl" class="lcsb.mapviewer.api.comment.CommentRestImpl"/>
-
+	<bean id="CommentRestImpl" class="lcsb.mapviewer.api.projects.comments.CommentRestImpl"/>
 	<bean id="ProjectRestImpl" class="lcsb.mapviewer.api.projects.ProjectRestImpl"/>
 	<bean id="BioEntitiesRestImpl" class="lcsb.mapviewer.api.projects.models.bioEntities.BioEntitiesRestImpl"/>
 	<bean id="ChemicalRestImpl" class="lcsb.mapviewer.api.projects.chemicals.ChemicalRestImpl"/>
diff --git a/rest-api/src/test/java/lcsb/mapviewer/api/comment/CommentRestImplTest.java b/rest-api/src/test/java/lcsb/mapviewer/api/comment/CommentRestImplTest.java
index c37e1cc845..9b8e8de34a 100644
--- a/rest-api/src/test/java/lcsb/mapviewer/api/comment/CommentRestImplTest.java
+++ b/rest-api/src/test/java/lcsb/mapviewer/api/comment/CommentRestImplTest.java
@@ -11,6 +11,7 @@ import org.springframework.beans.factory.annotation.Autowired;
 
 import lcsb.mapviewer.api.QueryException;
 import lcsb.mapviewer.api.RestTestFunctions;
+import lcsb.mapviewer.api.projects.comments.CommentRestImpl;
 
 public class CommentRestImplTest extends RestTestFunctions {
 
-- 
GitLab