diff --git a/phis2-ws/src/main/java/opensilex/service/dao/AnnotationDAO.java b/phis2-ws/src/main/java/opensilex/service/dao/AnnotationDAO.java index 58e3a95aa..d506dfd7b 100644 --- a/phis2-ws/src/main/java/opensilex/service/dao/AnnotationDAO.java +++ b/phis2-ws/src/main/java/opensilex/service/dao/AnnotationDAO.java @@ -10,8 +10,13 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +import java.util.stream.Collectors; + import opensilex.service.dao.exception.DAOPersistenceException; + +import org.apache.jena.arq.querybuilder.ExprFactory; import org.apache.jena.arq.querybuilder.UpdateBuilder; +import org.apache.jena.arq.querybuilder.WhereBuilder; import org.apache.jena.datatypes.xsd.XSDDatatype; import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; @@ -19,6 +24,9 @@ import org.apache.jena.rdf.model.Property; import org.apache.jena.rdf.model.Resource; import org.apache.jena.rdf.model.ResourceFactory; +import org.apache.jena.sparql.core.TriplePath; +import org.apache.jena.sparql.path.Path; +import org.apache.jena.sparql.path.PathFactory; import org.apache.jena.vocabulary.DCTerms; import org.apache.jena.vocabulary.RDF; import org.eclipse.rdf4j.query.BindingSet; @@ -27,6 +35,9 @@ import org.eclipse.rdf4j.query.QueryLanguage; import org.eclipse.rdf4j.query.TupleQuery; import org.eclipse.rdf4j.query.TupleQueryResult; +import org.eclipse.rdf4j.query.Update; +import org.eclipse.rdf4j.query.UpdateExecutionException; +import org.eclipse.rdf4j.repository.RepositoryConnection; import org.eclipse.rdf4j.repository.RepositoryException; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; @@ -501,10 +512,114 @@ private ArrayList getAnnotationsWithoutBodyValuesFromResult(TupleQue return annotations; } - @Override - public void delete(List objects) throws DAOPersistenceException, Exception { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. - } + /** + * @return an {@link UpdateBuilder} producing a SPARQL query which remove all annotation having only + * the given annotation as target. + * @example + *
+     * DELETE { ?src_a ?src_a_pred ?src_a_obj . } 
+     * WHERE { 
+     *     ?src_a oa:hasTarget+ "http://www.phenome-fppn.fr/test/id/annotation/1e331ca4-2e63-4728-8373-050a2b51c3dc". 
+     *     ?src_a ?src_a_pred ?src_a_obj
+     * 	   MINUS { 
+     *     	  ?src_a oa:hasTarget ?s2, ?s3 . 
+     *     	  FILTER( ?s2 != ?s3) 
+     * 	  } 
+     * } 
+     * 
+ * @param annotationUri : the annotation on which we check if there exist an another annotation + * @throws RepositoryException + * @throws UpdateExecutionException + */ + protected UpdateBuilder getRemoveAllSuperAnnotationQuery(String annotationUri) throws RepositoryException, UpdateExecutionException { + + Node srcAnnotation = NodeFactory.createVariable("src_a"), + srcAnnotationTarget = NodeFactory.createVariable("src_a_target"), + srcAnnotationTarget2 = NodeFactory.createVariable("src_a_target2"), + srcAnnotationPredicate = NodeFactory.createVariable("src_a_pred"), + srcAnnotationObject = NodeFactory.createVariable("src_a_obj"), + oaTargetPred = NodeFactory.createURI(Oa.RELATION_HAS_TARGET.toString()), + targetAnnotation = NodeFactory.createURI(annotationUri); + + Path oaTargetPath = PathFactory.pathOneOrMore1(PathFactory.pathLink(oaTargetPred)); // create the property path (oa:target)+ + + return new UpdateBuilder() + .addDelete(srcAnnotation, srcAnnotationPredicate, srcAnnotationObject) + .addWhere(new TriplePath(srcAnnotation, oaTargetPath, targetAnnotation) ) + .addWhere(srcAnnotation, srcAnnotationPredicate, srcAnnotationObject) + + // add the minus clause in order to check if the annotation has more than one target + .addMinus(new WhereBuilder() + .addWhere(srcAnnotation, oaTargetPred, srcAnnotationTarget) + .addWhere(srcAnnotation, oaTargetPred, srcAnnotationTarget2) + .addFilter(new ExprFactory().ne(srcAnnotationTarget, srcAnnotationTarget2)) + ); + } + + /** + * @return an {@link UpdateBuilder} producing a SPARQL query which remove all incoming and outcoming + * annotation triples + * @example + *
+      * DELETE { 
+      * 		http://www.phenome-fppn.fr/test/id/annotation/1e331ca4-2e63-4728-8373-050a2b51c3dc ?p ?o .  
+      * 		?s ?p1 http://www.phenome-fppn.fr/test/id/annotation/1e331ca4-2e63-4728-8373-050a2b51c3dc   
+      * } WHERE {  
+      * 		{ http://www.phenome-fppn.fr/test/id/annotation/1e331ca4-2e63-4728-8373-050a2b51c3dc ?p ?o }  
+      *   UNION  
+      * 		{?s ?p1 http://www.phenome-fppn.fr/test/id/annotation/1e331ca4-2e63-4728-8373-050a2b51c3dc }  
+      * }  
+      * 
+ * @param annotationUri : the URI of the {@link Annotation} to delete + * @throws RepositoryException + */ + protected UpdateBuilder getRemoveAllAnnotationTripleQuery(String annotationUri) throws RepositoryException { + + Node subject = NodeFactory.createVariable("s"), + outPredicate = NodeFactory.createVariable("p_out"), + inPredicate = NodeFactory.createVariable("p_in"), + object = NodeFactory.createVariable("o"), + annotation = NodeFactory.createURI(annotationUri); + + return new UpdateBuilder() + .addDelete(annotation,outPredicate,object) + .addDelete(subject,inPredicate,annotation) + .addWhere(annotation, outPredicate, object) + .addUnion(new WhereBuilder().addWhere(subject,inPredicate,annotation)); + } + + /** + * @apiNote + * WARNING : delete an annotation trigger the deletion of all annotation which only have the annotation as target . + */ + @Override + protected void deleteAll(List annotationUris) throws RepositoryException, UpdateExecutionException { + + RepositoryConnection conn = getConnection(); + + for(String uri : annotationUris) { + + String removeIncomingsAnnotationQuery = getRemoveAllSuperAnnotationQuery(uri).buildRequest().toString(); + String removeAnnotationQuery = getRemoveAllAnnotationTripleQuery(uri).buildRequest().toString(); + + // first delete all annotation which has the annotationUri as target + Update update = conn.prepareUpdate(QueryLanguage.SPARQL,removeIncomingsAnnotationQuery); + update.execute(); + // then delete the annotation itself + update = conn.prepareUpdate(QueryLanguage.SPARQL,removeAnnotationQuery); + update.execute(); + } + } + + @Override + public void delete(List annotations) throws DAOPersistenceException, Exception, IllegalAccessException, IllegalAccessException { + + // get all annotation URIs into an ArrayList via Stream API + ArrayList uris = annotations.stream() + .map(annotation -> annotation.getUri()) + .collect(Collectors.toCollection(ArrayList::new)); + checkAndDeleteAll(uris); + } @Override public List update(List objects) throws DAOPersistenceException, Exception { diff --git a/phis2-ws/src/main/java/opensilex/service/dao/EventDAO.java b/phis2-ws/src/main/java/opensilex/service/dao/EventDAO.java index 003ed7d0f..507034aad 100644 --- a/phis2-ws/src/main/java/opensilex/service/dao/EventDAO.java +++ b/phis2-ws/src/main/java/opensilex/service/dao/EventDAO.java @@ -8,8 +8,14 @@ package opensilex.service.dao; import java.util.ArrayList; +import java.util.LinkedList; import java.util.List; +import java.util.stream.Collectors; + +import org.apache.jena.arq.querybuilder.ExprFactory; +import org.apache.jena.arq.querybuilder.SelectBuilder; import org.apache.jena.arq.querybuilder.UpdateBuilder; +import org.apache.jena.arq.querybuilder.WhereBuilder; import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; import org.apache.jena.rdf.model.Resource; @@ -21,6 +27,8 @@ import org.eclipse.rdf4j.query.QueryLanguage; import org.eclipse.rdf4j.query.TupleQuery; import org.eclipse.rdf4j.query.TupleQueryResult; +import org.eclipse.rdf4j.query.Update; +import org.eclipse.rdf4j.repository.RepositoryConnection; import org.eclipse.rdf4j.repository.RepositoryException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -33,6 +41,7 @@ import opensilex.service.dao.manager.Rdf4jDAO; import opensilex.service.model.User; import opensilex.service.ontology.Contexts; +import opensilex.service.ontology.Oa; import opensilex.service.ontology.Oeev; import opensilex.service.ontology.Rdf; import opensilex.service.ontology.Rdfs; @@ -634,10 +643,129 @@ public void addDeleteWhenUpdatingToUpdateBuilder(UpdateBuilder updateBuilder, Ev Oeev.concerns.getURI(), event.getConcernedItems()); } - + @Override - public void delete(List objects) throws DAOPersistenceException, Exception { - throw new UnsupportedOperationException("Not supported yet."); //To change body of generated methods, choose Tools | Templates. + public void delete(List events) throws DAOPersistenceException, Exception { + + // get all events URIs into an ArrayList via Stream API + List uris = events.stream() + .map(event -> event.getUri()) + .collect(Collectors.toCollection(ArrayList::new)); + checkAndDeleteAll(uris); + } + + /** + * @apiNote + * WARNING : delete an event trigger the deletion of all annotation which only have the event as target . + */ + @Override + protected void deleteAll(List uris) throws RepositoryException { + + AnnotationDAO annotationDAO = new AnnotationDAO(user); + RepositoryConnection conn = getConnection(); + + // make sure the two DAO use the same connection + annotationDAO.setConnection(conn); + + for(String eventUri : uris) { + + // get all annotation on event before deleting the event itself + List annotationUris = getAllAnnotationUrisWithEventAsTarget(eventUri); + + UpdateBuilder deleteEventQuery = deleteEventTriples(eventUri); + Update deleteEventUpdate = conn.prepareUpdate(QueryLanguage.SPARQL,deleteEventQuery.build().toString()); + deleteEventUpdate.execute(); + + // delete all annotation which have the given event as target + if(! annotationUris.isEmpty()) + annotationDAO.deleteAll(annotationUris); + } + } + + /** + * @return the {@link List} of {@link Annotation} which only have the given event uri as target. + * @example + * SELECT DISTINCT ?a + * WHERE { + * ?annotation oa:hasTarget "http://www.phenome-fppn.fr/id/event/5a1b3c0d-58af-4cfb-811e-e141b11453b1". + * MINUS { + * ?annotation oa:hasTarget ?target. + * FILTER ("http://www.phenome-fppn.fr/id/event/5a1b3c0d-58af-4cfb-811e-e141b11453b1" = ?target) + * } + * + * } + * @param eventUri + */ + protected List getAllAnnotationUrisWithEventAsTarget(String eventUri) { + + Node annotation = NodeFactory.createVariable("annotation"), + target = NodeFactory.createVariable("target"), + oaTargetPred = NodeFactory.createURI(Oa.RELATION_HAS_TARGET.toString()), + event = NodeFactory.createURI(eventUri), + annotationGraph = NodeFactory.createURI(Contexts.ANNOTATIONS.toString()); + + String removeAnnotationQuery = new SelectBuilder() + .addVar(annotation) + .addGraph(annotationGraph, new WhereBuilder() + .addWhere(annotation,oaTargetPred,event) + .addMinus(new WhereBuilder() + .addWhere(annotation,oaTargetPred,target) + .addFilter(new ExprFactory().ne(event, target))) + ) + .buildString(); + + List annotationUris = new LinkedList<>(); + TupleQuery getAnnotationQuery = getConnection().prepareTupleQuery(removeAnnotationQuery); + TupleQueryResult res = getAnnotationQuery.evaluate(); + + while(res.hasNext()) { + BindingSet bs = res.next(); + annotationUris.add(bs.getValue(annotation.getName()).stringValue()); + } + return annotationUris; + } + + /** + * @return an {@link UpdateBuilder} producing a SPARQL query which remove all event triples + * @example + *
+     * DELETE {
+     * 		"http://www.phenome-fppn.fr/id/event/5a1b3c0d-58af-4cfb-811e-e141b11453b1" ?p ?o .
+     * 	    ?s ?p_in "http://www.phenome-fppn.fr/id/event/5a1b3c0d-58af-4cfb-811e-e141b11453b1" .
+     *  	?time ?time_pred ?time_object 
+     * } WHERE { 
+     *		{ "http://www.phenome-fppn.fr/id/event/5a1b3c0d-58af-4cfb-811e-e141b11453b1" ?p_out ?o ;
+     *          																time:hasTime ?time. 
+     *       ?time ?time_pred ?time_object . }
+     *       UNION { ?s ?p_in "http://www.phenome-fppn.fr/id/event/5a1b3c0d-58af-4cfb-811e-e141b11453b1" }
+     *     }   
+     * }
+     * 
+ * @param eventUri : the URI of the {@link Event} to delete + */ + protected UpdateBuilder deleteEventTriples(String eventUri) { + + Node outPredicate = NodeFactory.createVariable("p_out"), + object = NodeFactory.createVariable("o"), + subject = NodeFactory.createVariable("s"), + inPredicate = NodeFactory.createVariable("p_in"), + time = NodeFactory.createVariable("time"), + timePred = NodeFactory.createVariable("time_pred"), + timeObj = NodeFactory.createVariable("time_object"); + + Node hasTimePred = NodeFactory.createURI(Time.hasTime.getURI()), + eventNode = NodeFactory.createURI(eventUri); + + return new UpdateBuilder() + .addDelete(eventNode,outPredicate,object) + .addDelete(time,timePred,timeObj) + .addDelete(subject,inPredicate,eventNode) + + .addWhere(eventNode,outPredicate,object) + .addWhere(eventNode,hasTimePred,time) + .addWhere(time,timePred,timeObj) + .addUnion( + new WhereBuilder().addWhere(subject,inPredicate,eventNode)); } @Override diff --git a/phis2-ws/src/main/java/opensilex/service/dao/manager/DAO.java b/phis2-ws/src/main/java/opensilex/service/dao/manager/DAO.java index 622b0ce54..5d3d51377 100644 --- a/phis2-ws/src/main/java/opensilex/service/dao/manager/DAO.java +++ b/phis2-ws/src/main/java/opensilex/service/dao/manager/DAO.java @@ -25,6 +25,23 @@ public abstract class DAO { */ public String remoteUserAdress; public User user; + + + public String getRemoteUserAdress() { + return remoteUserAdress; + } + + public void setRemoteUserAdress(String remoteUserAdress) { + this.remoteUserAdress = remoteUserAdress; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } /** * Creates in the storage the list of objects given. diff --git a/phis2-ws/src/main/java/opensilex/service/dao/manager/Rdf4jDAO.java b/phis2-ws/src/main/java/opensilex/service/dao/manager/Rdf4jDAO.java index 7b42af238..d2208da4c 100644 --- a/phis2-ws/src/main/java/opensilex/service/dao/manager/Rdf4jDAO.java +++ b/phis2-ws/src/main/java/opensilex/service/dao/manager/Rdf4jDAO.java @@ -13,6 +13,9 @@ import java.util.Map; import javax.ws.rs.WebApplicationException; import javax.ws.rs.core.Response; + +import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.StringUtils; import org.apache.jena.arq.querybuilder.UpdateBuilder; import org.apache.jena.graph.Node; import org.apache.jena.graph.NodeFactory; @@ -42,6 +45,7 @@ import opensilex.service.configuration.DateFormat; import opensilex.service.configuration.DefaultBrapiPaginationValues; import opensilex.service.configuration.URINamespaces; +import opensilex.service.dao.UserDAO; import opensilex.service.dao.exception.DAOPersistenceException; import opensilex.service.documentation.StatusCodeMsg; import opensilex.service.model.User; @@ -163,7 +167,15 @@ public Rdf4jDAO(String repositoryID) { } } + /** + * + * @return the {@link RepositoryConnection} used by the DAO. + * Initialize the connection if not done yet. + * see {@link #initConnection()}. + */ public RepositoryConnection getConnection() { + if(connection == null || ! connection.isOpen()) + initConnection(); return connection; } @@ -586,29 +598,89 @@ public List findLabelsForUri(String uri) { return labels; } + + /** + * Delete a list of objects into the TripleStore. + * @param uris : a {@link List} of objects URI + * @throws RepositoryException + * @throws UpdateExecutionException + */ + protected void deleteAll(List uris) throws RepositoryException, UpdateExecutionException { + throw new NotImplementedException(""); + } + + /** + * Delete a list of objects into the TripleStore. + * @param uris : the list of URI to delete + * @throws IllegalArgumentException if the {@link #user} is not an Admin user or if a given URI is not present + * into the TripleStore. + * + * @throws Exception + * + * @see #deleteAll(List) + * @see UserDAO#isAdmin(User) + */ + public void checkAndDeleteAll(List uris) throws IllegalArgumentException, Exception { + + if(user == null || StringUtils.isEmpty(user.getAdmin())) { + throw new IllegalArgumentException("No user/bad user provided"); + } + // check if the user has the right to delete objects + if(! new UserDAO().isAdmin(user)) { + throw new IllegalArgumentException("Error : only an admin user can delete an object"); + } + + StringBuilder errorMsgs = new StringBuilder(); + boolean allUriExists = true; + for(String uri : uris) { + if(! existUri(uri)) { + errorMsgs.append(uri+" , "); + allUriExists = false; + } + } + if(!allUriExists) { + throw new IllegalArgumentException(errorMsgs.append(" don't belongs to the TripleStore").toString()); + } + + try { + startTransaction(); + deleteAll(uris); + commitTransaction(); + } catch(Exception e) { + rollbackTransaction(); + throw e; + } + } @Override - protected void initConnection() { - getConnection().begin(); + protected void initConnection() { + if(connection == null || ! connection.isOpen()) + connection = rep.getConnection(); } @Override protected void closeConnection() { - getConnection().close(); + if(connection != null && connection.isOpen()) + connection.close(); } @Override protected void startTransaction() { - // transactions starts automatically in SPARQL. + // initialize the connection if not done + initConnection(); + if(! connection.isActive()) + connection.begin(); } @Override protected void commitTransaction() { - getConnection().commit(); + if(connection != null && connection.isActive()) + connection.commit(); } @Override protected void rollbackTransaction() { - getConnection().rollback(); + if(connection != null && connection.isActive()) + connection.rollback(); } } diff --git a/phis2-ws/src/main/java/opensilex/service/documentation/DocumentationAnnotation.java b/phis2-ws/src/main/java/opensilex/service/documentation/DocumentationAnnotation.java index 1044a5749..2d0b091d7 100644 --- a/phis2-ws/src/main/java/opensilex/service/documentation/DocumentationAnnotation.java +++ b/phis2-ws/src/main/java/opensilex/service/documentation/DocumentationAnnotation.java @@ -30,6 +30,7 @@ public final class DocumentationAnnotation { public static final String PAGE_SIZE_MONGO = "Number of elements per page (limited to 1000000)"; // User messages + public static final String INTERNAL_SERVER_ERROR = "Internal server error."; public static final String ERROR_SEND_DATA = "Server error. Cannot send data."; public static final String ERROR_FETCH_DATA = "Server error. Cannot fetch data."; public static final String BAD_USER_INFORMATION = "Bad informations send by user"; @@ -299,6 +300,7 @@ public final class DocumentationAnnotation { public static final String EXAMPLE_CALL_DATATYPE = "json"; + public static final String ANNOTATION_URI_DEFINITION = "An annotation URI (Unique Resource Identifier)"; public static final String EXAMPLE_ANNOTATION_URI = "http://www.opensilex.org/platform/id/annotation/8247af37-769c-495b-8e7e-78b1141176c2"; public static final String EXAMPLE_ANNOTATION_TARGET = "http://www.opensilex.org/demo/id/variable/v0000001"; public static final String EXAMPLE_ANNOTATION_MOTIVATED_BY = "http://www.w3.org/ns/oa#commenting"; diff --git a/phis2-ws/src/main/java/opensilex/service/resource/AnnotationResourceService.java b/phis2-ws/src/main/java/opensilex/service/resource/AnnotationResourceService.java index f7414232f..d2193cca8 100644 --- a/phis2-ws/src/main/java/opensilex/service/resource/AnnotationResourceService.java +++ b/phis2-ws/src/main/java/opensilex/service/resource/AnnotationResourceService.java @@ -19,6 +19,9 @@ import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -36,6 +39,7 @@ import opensilex.service.documentation.DocumentationAnnotation; import opensilex.service.documentation.StatusCodeMsg; import opensilex.service.view.brapi.form.ResponseFormPOST; +import opensilex.service.resource.dto.DeleteDTO; import opensilex.service.resource.dto.annotation.AnnotationDTO; import opensilex.service.resource.dto.annotation.AnnotationPostDTO; import opensilex.service.resource.validation.interfaces.URL; @@ -57,9 +61,9 @@ @Path("/annotations") public class AnnotationResourceService extends ResourceService { final static Logger LOGGER = LoggerFactory.getLogger(SensorResourceService.class); - + public final static String EMPTY_ANNOTATION_LIST = "the annotation list to add is empty"; - + /** * Inserts the given annotations in the triplestore. * @param annotationsDtos annotationsDtos @@ -82,36 +86,36 @@ public class AnnotationResourceService extends ResourceService { @POST @ApiOperation(value = "Post annotations", notes = "Register new annotations in the triplestore") @ApiResponses(value = { - @ApiResponse(code = 201, message = "Annotations saved", response = ResponseFormPOST.class), - @ApiResponse(code = 400, message = DocumentationAnnotation.BAD_USER_INFORMATION), - @ApiResponse(code = 401, message = DocumentationAnnotation.USER_NOT_AUTHORIZED), - @ApiResponse(code = 500, message = DocumentationAnnotation.ERROR_SEND_DATA) + @ApiResponse(code = 201, message = "Annotations saved", response = ResponseFormPOST.class), + @ApiResponse(code = 400, message = DocumentationAnnotation.BAD_USER_INFORMATION), + @ApiResponse(code = 401, message = DocumentationAnnotation.USER_NOT_AUTHORIZED), + @ApiResponse(code = 500, message = DocumentationAnnotation.ERROR_SEND_DATA) }) @ApiImplicitParams({ - @ApiImplicitParam(name = GlobalWebserviceValues.AUTHORIZATION, required = true, - dataType = GlobalWebserviceValues.DATA_TYPE_STRING, paramType = GlobalWebserviceValues.HEADER, - value = DocumentationAnnotation.ACCES_TOKEN, - example = GlobalWebserviceValues.AUTHENTICATION_SCHEME + " ") + @ApiImplicitParam(name = GlobalWebserviceValues.AUTHORIZATION, required = true, + dataType = GlobalWebserviceValues.DATA_TYPE_STRING, paramType = GlobalWebserviceValues.HEADER, + value = DocumentationAnnotation.ACCES_TOKEN, + example = GlobalWebserviceValues.AUTHENTICATION_SCHEME + " ") }) public Response post( - @ApiParam(value = DocumentationAnnotation.ANNOTATION_POST_DATA_DEFINITION) + @ApiParam(value = DocumentationAnnotation.ANNOTATION_POST_DATA_DEFINITION) @Valid ArrayList annotationsDtos, - @Context HttpServletRequest context) { - + @Context HttpServletRequest context) { + // Set DAO AnnotationDAO objectDao = new AnnotationDAO(userSession.getUser()); if (context.getRemoteAddr() != null) { objectDao.remoteUserAdress = context.getRemoteAddr(); } - + return getPostResponse(objectDao, annotationsDtos, context.getRemoteAddr(), StatusCodeMsg.EMPTY_ANNOTATION_LIST); } /** * Searches annotations by URI, creator, comment, date of creation, target. - * @example { - * "metadata": { - * "pagination": { + * @example { + * "metadata": { + * "pagination": { * "pageSize": 20, * "currentPage": 0, * "totalCount": 297, @@ -120,8 +124,8 @@ public Response post( * "status": [], * "datafiles": [] * }, - * "result": { - * "data": [ { + * "result": { + * "data": [ { * "uri": "http://www.phenome-fppn.fr/platform/id/annotation/8247af37-769c-495b-8e7e-78b1141176c2", * "creator": "http://www.phenome-fppn.fr/diaphen/id/agent/arnaud_charleroy", * "creationDate": "2018-06-22 14:54:42+0200", @@ -143,16 +147,16 @@ public Response post( @ApiOperation(value = "Get all annotations corresponding to the search params given", notes = "Retrieve all annotations authorized for the user corresponding to the searched params given") @ApiResponses(value = { - @ApiResponse(code = 200, message = "Retrieve all annotations", response = AnnotationDTO.class, responseContainer = "List"), - @ApiResponse(code = 400, message = DocumentationAnnotation.BAD_USER_INFORMATION), - @ApiResponse(code = 401, message = DocumentationAnnotation.USER_NOT_AUTHORIZED), - @ApiResponse(code = 500, message = DocumentationAnnotation.ERROR_FETCH_DATA) + @ApiResponse(code = 200, message = "Retrieve all annotations", response = AnnotationDTO.class, responseContainer = "List"), + @ApiResponse(code = 400, message = DocumentationAnnotation.BAD_USER_INFORMATION), + @ApiResponse(code = 401, message = DocumentationAnnotation.USER_NOT_AUTHORIZED), + @ApiResponse(code = 500, message = DocumentationAnnotation.ERROR_FETCH_DATA) }) @ApiImplicitParams({ - @ApiImplicitParam(name = GlobalWebserviceValues.AUTHORIZATION, required = true, - dataType = GlobalWebserviceValues.DATA_TYPE_STRING, paramType = GlobalWebserviceValues.HEADER, - value = DocumentationAnnotation.ACCES_TOKEN, - example = GlobalWebserviceValues.AUTHENTICATION_SCHEME + " ") + @ApiImplicitParam(name = GlobalWebserviceValues.AUTHORIZATION, required = true, + dataType = GlobalWebserviceValues.DATA_TYPE_STRING, paramType = GlobalWebserviceValues.HEADER, + value = DocumentationAnnotation.ACCES_TOKEN, + example = GlobalWebserviceValues.AUTHENTICATION_SCHEME + " ") }) @Produces(MediaType.APPLICATION_JSON) public Response getAnnotationsBySearch( @@ -168,8 +172,8 @@ public Response getAnnotationsBySearch( ArrayList annotations; try { annotations = annotationDao.find(uri, creator, target, bodyValue, motivatedBy, page, pageSize); - - // handle search exceptions + + // handle search exceptions } catch (DAOPersistenceException ex) { LOGGER.error(ex.getMessage(), ex); return getResponseWhenPersistenceError(ex); @@ -185,8 +189,8 @@ public Response getAnnotationsBySearch( try { int totalCount = annotationDao.count(uri, creator, target, bodyValue, motivatedBy); return getGETResponseWhenSuccess(annotations, pageSize, page, totalCount); - - // handle count exceptions + + // handle count exceptions } catch (DAOPersistenceException ex) { LOGGER.error(ex.getMessage(), ex); return getResponseWhenPersistenceError(ex); @@ -202,8 +206,8 @@ public Response getAnnotationsBySearch( * @example * { * "metadata": { "pagination": null, "status": [], "datafiles": [] }, - * "result": { - * "data": [ { + * "result": { + * "data": [ { * "uri": "http://www.phenome-fppn.fr/platform/id/annotation/8247af37-769c-495b-8e7e-78b1141176c2", * "creator": "http://www.phenome-fppn.fr/diaphen/id/agent/arnaud_charleroy", * "creationDate": "2018-06-22 14:54:42+0200", @@ -222,58 +226,112 @@ public Response getAnnotationsBySearch( @ApiOperation(value = "Get a annotation", notes = "Retrieve a annotation. Need URL encoded annotation URI") @ApiResponses(value = { - @ApiResponse( - code = 200, - message = "Retrieve a annotation", - response = AnnotationDTO.class, - responseContainer = "List"), - @ApiResponse(code = 400, message = DocumentationAnnotation.BAD_USER_INFORMATION), - @ApiResponse(code = 401, message = DocumentationAnnotation.USER_NOT_AUTHORIZED), - @ApiResponse(code = 500, message = DocumentationAnnotation.ERROR_FETCH_DATA) + @ApiResponse( + code = 200, + message = "Retrieve a annotation", + response = AnnotationDTO.class, + responseContainer = "List"), + @ApiResponse(code = 400, message = DocumentationAnnotation.BAD_USER_INFORMATION), + @ApiResponse(code = 401, message = DocumentationAnnotation.USER_NOT_AUTHORIZED), + @ApiResponse(code = 500, message = DocumentationAnnotation.ERROR_FETCH_DATA) }) @ApiImplicitParams({ - @ApiImplicitParam(name = GlobalWebserviceValues.AUTHORIZATION, required = true, - dataType = GlobalWebserviceValues.DATA_TYPE_STRING, paramType = GlobalWebserviceValues.HEADER, - value = DocumentationAnnotation.ACCES_TOKEN, - example = GlobalWebserviceValues.AUTHENTICATION_SCHEME + " ") + @ApiImplicitParam(name = GlobalWebserviceValues.AUTHORIZATION, required = true, + dataType = GlobalWebserviceValues.DATA_TYPE_STRING, paramType = GlobalWebserviceValues.HEADER, + value = DocumentationAnnotation.ACCES_TOKEN, + example = GlobalWebserviceValues.AUTHENTICATION_SCHEME + " ") }) @Produces(MediaType.APPLICATION_JSON) public Response getAnnotationByUri( @ApiParam( - value = DocumentationAnnotation.SENSOR_URI_DEFINITION, - required = true, - example = DocumentationAnnotation.EXAMPLE_SENSOR_URI) - @URL @PathParam("uri") String uri) { - + value = DocumentationAnnotation.SENSOR_URI_DEFINITION, + required = true, + example = DocumentationAnnotation.EXAMPLE_SENSOR_URI) + @URL @PathParam("uri") String uri) { + return getGETByUriResponseFromDAOResults(new AnnotationDAO(userSession.getUser()), uri); } @Override protected ArrayList getDTOsFromObjects(List objects) { - ArrayList dtos = new ArrayList(); + ArrayList dtos = new ArrayList<>(); // Generate DTOs objects.forEach((object) -> { dtos.add(new AnnotationDTO((Annotation)object)); }); return dtos; } - + @Override protected List getObjectsFromDTOs (List dtos) throws Exception { - List objects = new ArrayList<>(); + List objects = new ArrayList<>(dtos.size()); for (AbstractVerifiedClass objectDto : dtos) { objects.add((Annotation)objectDto.createObjectFromDTO()); } return objects; } - + @Override protected List getUrisFromObjects (List createdObjects) { - List createdUris = new ArrayList<>(); + List createdUris = new ArrayList<>(createdObjects.size()); createdObjects.forEach(object -> { createdUris.add(((Annotation)object).getUri()); }); return createdUris; } + + /** + * Delete each {@link Annotation} URI from {@link DeleteDTO#getUris()} + * @param deleteDTO : the DTO which contains the list of Annotation URI to delete + * @param context + * @return + * + * @example + *[ + * http://www.phenome-fppn.fr/platform/id/annotation/8247af37-769c-495b-8e7e-78b1141176c2, + * http://www.phenome-fppn.fr/platform/id/annotation/8247gt37-769c-495b-8e7e-91jh633151k4 + *] + * + */ + @DELETE + @Path("{uri}") + @ApiOperation( + value = "Delete a list of annotation", + notes = "Delete a list of annotation. Need URL encoded annotation URI" + + ) + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Annotation(s) deleted", response = ResponseFormPOST.class), + @ApiResponse(code = 400, message = DocumentationAnnotation.BAD_USER_INFORMATION), + @ApiResponse(code = 401, message = DocumentationAnnotation.USER_NOT_AUTHORIZED), + @ApiResponse(code = 500, message = DocumentationAnnotation.INTERNAL_SERVER_ERROR), + }) + @ApiImplicitParams({ + @ApiImplicitParam( + name = GlobalWebserviceValues.AUTHORIZATION, + required = true, + dataType = GlobalWebserviceValues.DATA_TYPE_STRING, + paramType = GlobalWebserviceValues.HEADER, + value = DocumentationAnnotation.ACCES_TOKEN, + example = GlobalWebserviceValues.AUTHENTICATION_SCHEME + " ") + }) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response deleteAnnotationByUri( + @ApiParam( + value = DocumentationAnnotation.ANNOTATION_URI_DEFINITION, + required = true, + example = DocumentationAnnotation.EXAMPLE_ANNOTATION_URI + ) + @Valid @NotNull DeleteDTO deleteDTO, @Context HttpServletRequest context) { + + AnnotationDAO annotationDAO = new AnnotationDAO(userSession.getUser()); + if (context.getRemoteAddr() != null) { + annotationDAO.setRemoteUserAdress(context.getRemoteAddr()); + } + Response response = buildDeleteObjectsByUriResponse(annotationDAO, deleteDTO,"Annotation(s) deleted"); + annotationDAO.getConnection().close(); + return response; + } } diff --git a/phis2-ws/src/main/java/opensilex/service/resource/EventResourceService.java b/phis2-ws/src/main/java/opensilex/service/resource/EventResourceService.java index 0e5f9a9e3..f45606228 100644 --- a/phis2-ws/src/main/java/opensilex/service/resource/EventResourceService.java +++ b/phis2-ws/src/main/java/opensilex/service/resource/EventResourceService.java @@ -19,7 +19,9 @@ import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import javax.validation.constraints.Min; +import javax.validation.constraints.NotNull; import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; @@ -40,6 +42,7 @@ import opensilex.service.dao.exception.DAOPersistenceException; import opensilex.service.documentation.DocumentationAnnotation; import opensilex.service.documentation.StatusCodeMsg; +import opensilex.service.resource.dto.DeleteDTO; import opensilex.service.resource.dto.event.EventGetDTO; import opensilex.service.resource.dto.event.EventPostDTO; import opensilex.service.resource.dto.rdfResourceDefinition.RdfResourceDefinitionDTO; @@ -435,7 +438,7 @@ public Response put( @Override protected ArrayList getDTOsFromObjects(List objects) { - ArrayList dtos = new ArrayList(); + ArrayList dtos = new ArrayList<>(objects.size()); objects.forEach((object) -> { dtos.add(new EventGetDTO((Event)object)); }); @@ -445,7 +448,7 @@ protected ArrayList getDTOsFromObjects(List getObjectsFromDTOs (List dtos) throws Exception { - List objects = new ArrayList<>(); + List objects = new ArrayList<>(dtos.size()); for (AbstractVerifiedClass objectDto : dtos) { objects.add((Event)objectDto.createObjectFromDTO()); } @@ -460,4 +463,56 @@ protected List getUrisFromObjects (List createdObjects }); return createdUris; } + + /** + * Delete each {@link Event} URI from {@link DeleteDTO#getUris()} + * @param deleteDTO : the DTO which contains the list of Event URI to delete + * @param context + * @return + * + * @example + *[ + * http://www.phenome-fppn.fr/platform/id/event/8247af37-769c-495b-8e7e-78b1141176c2, + * http://www.phenome-fppn.fr/platform/id/event/8247gt37-769c-495b-8e7e-91jh633151k4 + *] + * + */ + @DELETE + @Path("{uri}") + @ApiOperation( + value = "Delete a list of event", + notes = "Delete a list of event. Need URL encoded event URI") + @ApiResponses(value = { + @ApiResponse(code = 200, message = "Event(s) deleted", response = ResponseFormPOST.class), + @ApiResponse(code = 400, message = DocumentationAnnotation.BAD_USER_INFORMATION), + @ApiResponse(code = 401, message = DocumentationAnnotation.USER_NOT_AUTHORIZED), + @ApiResponse(code = 500, message = DocumentationAnnotation.INTERNAL_SERVER_ERROR), + }) + @ApiImplicitParams({ + @ApiImplicitParam( + name = GlobalWebserviceValues.AUTHORIZATION, + required = true, + dataType = GlobalWebserviceValues.DATA_TYPE_STRING, + paramType = GlobalWebserviceValues.HEADER, + value = DocumentationAnnotation.ACCES_TOKEN, + example = GlobalWebserviceValues.AUTHENTICATION_SCHEME + " ") + }) + @Consumes(MediaType.APPLICATION_JSON) + @Produces(MediaType.APPLICATION_JSON) + public Response deleteEventUri( + @ApiParam( + value = DocumentationAnnotation.EVENT_URI_DEFINITION, + required = true, + example = DocumentationAnnotation.EXAMPLE_EVENT_URI + ) + @Valid @NotNull DeleteDTO deleteDTO, @Context HttpServletRequest context) { + + EventDAO eventDao = new EventDAO(userSession.getUser()); + if (context.getRemoteAddr() != null) { + eventDao.setRemoteUserAdress(context.getRemoteAddr()); + } + Response response = buildDeleteObjectsByUriResponse(eventDao, deleteDTO,"Event(s) deleted"); + eventDao.getConnection().close(); + return response; + } } diff --git a/phis2-ws/src/main/java/opensilex/service/resource/ResourceService.java b/phis2-ws/src/main/java/opensilex/service/resource/ResourceService.java index 8d0b042c0..83ce56c0c 100644 --- a/phis2-ws/src/main/java/opensilex/service/resource/ResourceService.java +++ b/phis2-ws/src/main/java/opensilex/service/resource/ResourceService.java @@ -9,6 +9,8 @@ import java.util.ArrayList; import java.util.List; +import java.util.Objects; + import javax.ws.rs.core.Response; import opensilex.service.PropertiesFileManager; import opensilex.service.authentication.Session; @@ -16,9 +18,12 @@ import opensilex.service.dao.exception.DAOPersistenceException; import opensilex.service.dao.exception.ResourceAccessDeniedException; import opensilex.service.dao.manager.DAO; +import opensilex.service.dao.manager.Rdf4jDAO; import opensilex.service.documentation.StatusCodeMsg; import opensilex.service.injection.SessionInject; import static opensilex.service.resource.DocumentResourceService.LOGGER; + +import opensilex.service.resource.dto.DeleteDTO; import opensilex.service.resource.dto.manager.AbstractVerifiedClass; import opensilex.service.view.brapi.Status; import opensilex.service.result.ResultForm; @@ -159,7 +164,7 @@ protected Response getGETResponseWhenNoResult() { * @return */ protected Response getGETByUriResponseFromDAOResults(DAO dao, String uri) { - ArrayList objects = new ArrayList(); + ArrayList objects = new ArrayList<>(); try { Object object = dao.findById(uri); objects.add(object); @@ -346,4 +351,45 @@ private Response getPostPutResponseFromMultipleOperationStatus (Response.Status private Response buildResponse(Response.Status status, AbstractResultForm resultForm) { return Response.status(status).entity(resultForm).build(); } + + /** + * + * @param e + * @return + */ + protected Response buildResponseFromException(Exception e) { + + Response.Status status = null; + if(e instanceof IllegalArgumentException) { + status = Response.Status.BAD_REQUEST; + } + else if(e instanceof IllegalAccessException) { + status = Response.Status.FORBIDDEN; + } + else { + status = Response.Status.INTERNAL_SERVER_ERROR; + } + AbstractResultForm putResponse = new ResponseFormPOST(new Status(e.getMessage())); + return Response.status(status).entity(putResponse).build(); + } + + /** + * @param dao : a {@link Rdf4jDAO} used to delete a {@link List} of String URI. + * @param deleteDTO : a {@link DeleteDTO} which contains a {@link List} of URI. + * @param msg : delete message + * @return + */ + protected Response buildDeleteObjectsByUriResponse(Rdf4jDAO dao, DeleteDTO deleteDTO, String msg) { + + Objects.requireNonNull(dao); + Objects.requireNonNull(deleteDTO); + try { + dao.checkAndDeleteAll(deleteDTO.getUris()); + ResponseFormPOST resp = new ResponseFormPOST(new Status(msg,null)); + return buildResponse(Response.Status.OK,resp); + } catch(Exception e) { + return buildResponseFromException(e); + } + + } } diff --git a/phis2-ws/src/main/java/opensilex/service/resource/dto/DeleteDTO.java b/phis2-ws/src/main/java/opensilex/service/resource/dto/DeleteDTO.java new file mode 100644 index 000000000..b76108da1 --- /dev/null +++ b/phis2-ws/src/main/java/opensilex/service/resource/dto/DeleteDTO.java @@ -0,0 +1,52 @@ +//****************************************************************************** +// DeleteDTO.java +// SILEX-PHIS +// Copyright © INRA 2019 +// Creation date: 25 oct. 2019 +// Contact: renaud.colin@inra.fr, anne.tireau@inra.fr, pascal.neveu@inra.fr +//****************************************************************************** + + +package opensilex.service.resource.dto; + +import java.util.List; + +import javax.validation.constraints.NotEmpty; +import javax.validation.constraints.NotNull; + +import io.swagger.annotations.ApiModel; +import io.swagger.annotations.ApiModelProperty; +import opensilex.service.resource.dto.manager.AbstractVerifiedClass; +import opensilex.service.resource.validation.interfaces.URL; + +/** + * @author renaud.colin@inra.fr + * + */ +@ApiModel +public class DeleteDTO extends AbstractVerifiedClass { + + /** + * URI(s) of objects to delete + * @example http://www.phenome-fppn.fr/platform/id/annotation/8247af37-769c-495b-8e7e-78b1141176c2 + */ + private List uris; + + @Override + public Object createObjectFromDTO() throws Exception { + throw new UnsupportedOperationException("No object could be created from a DeleteDTO"); + } + + @URL + @NotNull + @NotEmpty + @ApiModelProperty(notes = "Need to be a list of URI") + public List getUris() { + return uris; + } + + public void setUris(List uris) { + this.uris = uris; + } + +} diff --git a/phis2-ws/src/main/java/opensilex/service/view/brapi/Status.java b/phis2-ws/src/main/java/opensilex/service/view/brapi/Status.java index 284c29222..33804e86f 100644 --- a/phis2-ws/src/main/java/opensilex/service/view/brapi/Status.java +++ b/phis2-ws/src/main/java/opensilex/service/view/brapi/Status.java @@ -18,8 +18,12 @@ public class Status { public Status() { } + + public Status(String message) { + this.message = message; + } - public Status(String message, StatusException exception) { + public Status(String message, StatusException exception) { this.exception = exception; this.message = message; } diff --git a/phis2-ws/src/test/java/opensilex/service/dao/AnnotationDAOTest.java b/phis2-ws/src/test/java/opensilex/service/dao/AnnotationDAOTest.java new file mode 100644 index 000000000..8e368e0ca --- /dev/null +++ b/phis2-ws/src/test/java/opensilex/service/dao/AnnotationDAOTest.java @@ -0,0 +1,169 @@ +//****************************************************************************** +// AnnotationDAOTest.java +// SILEX-PHIS +// Copyright © INRA 2019 +// Creation date: 22 oct. 2019 +// Contact: renaud.colin@inra.fr, anne.tireau@inra.fr, pascal.neveu@inra.fr +//****************************************************************************** + + +package opensilex.service.dao; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.LinkedList; +import java.util.List; + +import org.joda.time.DateTime; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import opensilex.service.dao.exception.DAOPersistenceException; +import opensilex.service.dao.manager.Rdf4jDAO; +import opensilex.service.dao.manager.Rdf4jDAOTest; +import opensilex.service.model.Annotation; +import opensilex.service.model.Event; +import opensilex.service.model.Experiment; +import opensilex.service.model.Project; +import opensilex.service.model.ScientificObject; +import opensilex.service.ontology.Oa; + +public class AnnotationDAOTest extends Rdf4jDAOTest { + + /** + * The {@link Rdf4jDAO} to test + */ + private static AnnotationDAO annotationDao; + private static List events; + private static ScientificObject so; + private static String userUri; + + @BeforeAll + public static void setup() throws DAOPersistenceException, Exception { + + UserDAO userDao = new UserDAO(); + userUri = "http://www.opensilex.org/demo/id/agent/admin_phis"; + + annotationDao = new AnnotationDAO(userDao.findById(userUri)); + initDaoWithInMemoryStoreConnection(annotationDao); + events = new LinkedList<>(); + } + + @BeforeEach + protected void initDao() throws DAOPersistenceException, Exception { + + Project createdProject = createAndGetProject(); + Experiment xp = createAndGetExperiment(createdProject); + so = createAndGetScientificObject(xp); + events.clear(); + + events.add(createAndGetEvent(so.getUri())); + } + + + @Test + /** + * Try to delete an annotation about one event . + * So the event annotation should be deleted + */ + void testDeleteAnnotation() throws DAOPersistenceException, Exception { + + long initialSize = annotationDao.getConnection().size(); + + Annotation a = createAndGetAnnotation(userUri,events.get(0).getUri()); + annotationDao.delete(Arrays.asList(a)); + + assertEquals(annotationDao.getConnection().size(), initialSize); + assertFalse(annotationDao.existUri(a.getUri())); + } + + @Test + /** + * Try to delete an annotation about one event. This annotation also have severals annotation. + * So the event annotation should not be deleted + */ + void testDeleteAnnotation_with_one_super_annotation() throws DAOPersistenceException, Exception { + + long initialSize = annotationDao.getConnection().size(); + + Annotation a = createAndGetAnnotation(userUri,events.get(0).getUri()); + Annotation a1 = createAndGetAnnotation(userUri,a.getUri()); // create annotation on the last created annotation + + annotationDao.delete(Arrays.asList(a)); + assertEquals(annotationDao.getConnection().size(), initialSize); + assertFalse(annotationDao.existUri(a.getUri())); + assertFalse(annotationDao.existUri(a1.getUri())); + } + + @Test + /** + * Try to delete an annotation about one event. This annotation is annotated + * by a annotation a1 which also have an annotation. + * A simple RDF representation would be + * , , ... , + * + * + */ + void testDeleteAnnotationWithRecursiveAnnotationChain() throws DAOPersistenceException, Exception { + + long initialSize = annotationDao.getConnection().size(); + + int k = 2; + String userUri = "http://www.opensilex.org/demo/id/agent/admin_phis"; + + ArrayList bodyValues = new ArrayList<>(); + bodyValues.add("annotate an event"); + ArrayList targets = new ArrayList<>(); + targets.add(events.get(0).getUri()); + + Annotation a = new Annotation(null,DateTime.now(),userUri,bodyValues,Oa.INSTANCE_DESCRIBING.toString(),targets); + + List annotationList = new ArrayList<>(); + annotationList.addAll(annotationDao.create(Arrays.asList(a))); // create the first annotation A and add it + + int nbTripleCreated = 6; + for(int i=1;i