View Javadoc
1   package de.dlr.shepard.context.collection.services;
2   
3   import de.dlr.shepard.auth.permission.services.PermissionsService;
4   import de.dlr.shepard.auth.security.AuthenticationContext;
5   import de.dlr.shepard.auth.users.entities.User;
6   import de.dlr.shepard.auth.users.services.UserService;
7   import de.dlr.shepard.common.exceptions.InvalidAuthException;
8   import de.dlr.shepard.common.exceptions.InvalidBodyException;
9   import de.dlr.shepard.common.exceptions.InvalidPathException;
10  import de.dlr.shepard.common.exceptions.InvalidRequestException;
11  import de.dlr.shepard.common.util.DateHelper;
12  import de.dlr.shepard.common.util.QueryParamHelper;
13  import de.dlr.shepard.context.collection.daos.DataObjectDAO;
14  import de.dlr.shepard.context.collection.entities.Collection;
15  import de.dlr.shepard.context.collection.entities.DataObject;
16  import de.dlr.shepard.context.collection.io.DataObjectIO;
17  import de.dlr.shepard.context.references.dataobject.daos.DataObjectReferenceDAO;
18  import de.dlr.shepard.context.references.dataobject.entities.DataObjectReference;
19  import de.dlr.shepard.context.version.services.VersionService;
20  import io.quarkus.logging.Log;
21  import jakarta.enterprise.context.RequestScoped;
22  import jakarta.inject.Inject;
23  import java.util.ArrayList;
24  import java.util.Arrays;
25  import java.util.Date;
26  import java.util.HashSet;
27  import java.util.List;
28  import java.util.Set;
29  import java.util.UUID;
30  import java.util.stream.Collectors;
31  
32  @RequestScoped
33  public class DataObjectService {
34  
35    @Inject
36    DataObjectDAO dataObjectDAO;
37  
38    @Inject
39    DataObjectReferenceDAO dataObjectReferenceDAO;
40  
41    @Inject
42    UserService userService;
43  
44    @Inject
45    DateHelper dateHelper;
46  
47    @Inject
48    VersionService versionService;
49  
50    @Inject
51    CollectionService collectionService;
52  
53    @Inject
54    PermissionsService permissionsService;
55  
56    @Inject
57    AuthenticationContext authenticationContext;
58  
59    /**
60     * Creates a DataObject
61     *
62     * @param collectionShepardId identifies the Collection
63     * @param dataObject          to be stored
64     * @return the stored DataObject with the auto generated id
65     * @throws InvalidPathException if collection with collectionShepardId does not
66     *                              exist
67     * @throws InvalidPathException if collection with collectionShepardId does not
68     *                              exist
69     * @throws InvalidBodyException if the list of successors is not null or not empty
70     */
71    public DataObject createDataObject(long collectionShepardId, DataObjectIO dataObject) throws InvalidBodyException {
72      Collection collection = collectionService.getCollection(collectionShepardId);
73      collectionService.assertIsAllowedToEditCollection(collectionShepardId);
74  
75      User user = userService.getCurrentUser();
76      DataObject parent = findRelatedDataObject(collection.getShepardId(), dataObject.getParentId(), null);
77      if (
78        dataObject.getSuccessorIds() != null && dataObject.getSuccessorIds().length != 0
79      ) throw new InvalidBodyException(
80        "when creating a new dataObject the list of successors must not be specified or be empty"
81      );
82      List<DataObject> predecessors = findRelatedDataObjects(
83        collection.getShepardId(),
84        dataObject.getPredecessorIds(),
85        null
86      );
87      DataObject toCreate = new DataObject();
88      toCreate.setAttributes(dataObject.getAttributes());
89      toCreate.setDescription(dataObject.getDescription());
90      toCreate.setName(dataObject.getName());
91      toCreate.setCollection(collection);
92      toCreate.setParent(parent);
93      toCreate.setPredecessors(predecessors);
94      toCreate.setCreatedAt(dateHelper.getDate());
95      toCreate.setCreatedBy(user);
96      DataObject created = dataObjectDAO.createOrUpdate(toCreate);
97      created.setShepardId(created.getId());
98      created = dataObjectDAO.createOrUpdate(created);
99      versionService.attachToVersionOfVersionableEntityAndReturnVersion(collectionShepardId, created.getShepardId());
100     return created;
101   }
102 
103   /**
104    * Get DataObject
105    *
106    * @param shepardId identifies the searched dataObject
107    * @return the DataObject with the given id
108    * @throws InvalidPathException if the DataObject cannot be found
109    * @throws InvalidAuthException if user does not have read permissions on the
110    *                              data object's collection
111    */
112   public DataObject getDataObject(long shepardId) {
113     return getDataObject(shepardId, null);
114   }
115 
116   /**
117    * Get DataObject
118    *
119    * @param shepardId  identifies the searched dataObject
120    * @param versionUID the dataobject's version UUID
121    * @return an Optional containing the DataObject with the given id
122    * @throws InvalidPathException if DataObject (with version UUID) cannot be
123    *                              found
124    * @throws InvalidAuthException if user does not have read permissions on the
125    *                              data object's collection
126    */
127   public DataObject getDataObject(long shepardId, UUID versionUID) {
128     DataObject ret;
129     String errorMsg;
130     if (versionUID == null) {
131       ret = dataObjectDAO.findByShepardId(shepardId);
132       errorMsg = "DataObject with id %s is null or deleted".formatted(shepardId);
133     } else {
134       ret = dataObjectDAO.findByShepardId(shepardId, versionUID);
135       errorMsg = "DataObject with id %s and versionUID %s is null or deleted".formatted(shepardId, versionUID);
136     }
137     if (ret == null || ret.isDeleted()) {
138       Log.error(errorMsg);
139       throw new InvalidPathException("ID ERROR - " + errorMsg);
140     }
141 
142     collectionService.assertIsAllowedToReadCollection(ret.getCollection().getShepardId());
143     cutDeleted(ret);
144 
145     HashSet<Long> incomingReferencesIdList = new HashSet<Long>();
146     for (DataObjectReference reference : ret.getIncoming()) incomingReferencesIdList.add(reference.getId());
147     List<DataObjectReference> completeIncomingReferences = new ArrayList<DataObjectReference>();
148     for (Long id : incomingReferencesIdList) completeIncomingReferences.add(dataObjectReferenceDAO.findByNeo4jId(id));
149 
150     HashSet<Long> childrenIdList = new HashSet<Long>();
151     for (DataObject child : ret.getChildren()) childrenIdList.add(child.getId());
152     List<DataObject> completeChildren = new ArrayList<DataObject>();
153     for (Long id : childrenIdList) completeChildren.add(dataObjectDAO.findByNeo4jId(id));
154 
155     HashSet<Long> predecessorsIdList = new HashSet<Long>();
156     for (DataObject predecessor : ret.getPredecessors()) predecessorsIdList.add(predecessor.getId());
157     List<DataObject> completePredecessors = new ArrayList<DataObject>();
158     for (Long id : predecessorsIdList) completePredecessors.add(dataObjectDAO.findByNeo4jId(id));
159 
160     HashSet<Long> successorsIdList = new HashSet<Long>();
161     for (DataObject successor : ret.getSuccessors()) successorsIdList.add(successor.getId());
162     List<DataObject> completeSuccessors = new ArrayList<DataObject>();
163     for (Long id : successorsIdList) completeSuccessors.add(dataObjectDAO.findByNeo4jId(id));
164 
165     ret.setChildren(completeChildren);
166     ret.setIncoming(completeIncomingReferences);
167     ret.setPredecessors(completePredecessors);
168     ret.setSuccessors(completeSuccessors);
169     if (ret.getParent() != null) ret.setParent(dataObjectDAO.findByNeo4jId(ret.getParent().getId()));
170     return ret;
171   }
172 
173   /**
174    * Get DataObject
175    *
176    * @param collectionShepardId collection's shepardId
177    * @param shepardId           identifies the searched dataObject
178    * @return the DataObject with the given id
179    * @throws InvalidPathException if dataobject or collection cannot be found or
180    *                              the dataobject does not match the collection
181    * @throws InvalidAuthException if user does not have read permissions on the
182    *                              collection
183    */
184   public DataObject getDataObject(long collectionShepardId, long shepardId) {
185     return getDataObject(collectionShepardId, shepardId, null);
186   }
187 
188   /**
189    * Get DataObject
190    *
191    * @param collectionShepardId collection's shepardId
192    * @param shepardId           identifies the searched dataObject
193    * @param versionUID          the DataObject's version UUID
194    * @return the DataObject with the given id
195    * @throws InvalidPathException if DataObject or collection cannot be found or
196    *                              the DataObject does not match the collection
197    * @throws InvalidAuthException if user does not have read permissions on the
198    *                              collection
199    */
200   public DataObject getDataObject(long shepardCollectionId, long shepardId, UUID versionUID) {
201     collectionService.getCollection(shepardCollectionId);
202 
203     DataObject dataObject;
204     try {
205       // This may throw a 403 if the data object is in a different collection for
206       // which the user does not have permissions -> handle that exception
207       // specifically
208       dataObject = getDataObject(shepardId, versionUID);
209     } catch (InvalidAuthException ex) {
210       throw new InvalidPathException("ID ERROR - There is no association between collection and dataObject");
211     }
212 
213     if (!dataObject.getCollection().getShepardId().equals(shepardCollectionId)) {
214       throw new InvalidPathException("ID ERROR - There is no association between collection and dataObject");
215     }
216     return dataObject;
217   }
218 
219   /**
220    * Searches the database for DataObjects.
221    *
222    * @param collectionShepardId  identifies the collection
223    * @param paramsWithShepardIds encapsulates possible parameters
224    * @param versionUID           identifies the version
225    * @return a List of DataObjects
226    * @throws InvalidPathException if collection with collectionShepardId does not
227    *                              exist
228    * @throws InvalidAuthException if user does not have read permissions on the
229    *                              collection
230    */
231   public List<DataObject> getAllDataObjectsByShepardIds(
232     long collectionShepardId,
233     QueryParamHelper paramsWithShepardIds,
234     UUID versionUID
235   ) {
236     collectionService.getCollection(collectionShepardId, versionUID);
237 
238     var unfiltered = dataObjectDAO.findByCollectionByShepardIds(collectionShepardId, paramsWithShepardIds, versionUID);
239     var dataObjects = unfiltered.stream().map(this::cutDeleted).toList();
240     return dataObjects;
241   }
242 
243   /**
244    * Updates a DataObject with new attributes. Hereby only not null attributes
245    * will replace the old attributes.
246    *
247    * @param collectionShepardId ShepardId of the collection the dataobject is
248    *                            assigned to
249    * @param dataObjectShepardId Identifies the dataObject
250    * @param dataObject          DataObject entity for updating.
251    * @return updated DataObject.
252    * @throws InvalidPathException if dataObject cannot be found or collection with
253    *                              collectionShepardId does not exist
254    * @throws InvalidAuthException if user does not have read or write permissions
255    *                              on the collection
256    * @throws InvalidBodyException if the list of successors is not admitted
257    */
258   public DataObject updateDataObject(long collectionShepardId, long dataObjectShepardId, DataObjectIO dataObject) {
259     DataObject old = getDataObject(collectionShepardId, dataObjectShepardId);
260     collectionService.assertIsAllowedToEditCollection(collectionShepardId);
261 
262     User user = userService.getCurrentUser();
263 
264     if (old.getParent() != null) dataObjectDAO.deleteHasChildRelation(
265       old.getParent().getShepardId(),
266       old.getShepardId()
267     );
268 
269     if (old.getPredecessors() != null) old
270       .getPredecessors()
271       .forEach(predecessor -> {
272         dataObjectDAO.deleteHasSuccessorRelation(predecessor.getShepardId(), old.getShepardId());
273       });
274 
275     if (dataObject.getSuccessorIds() != null) {
276       Set<Long> givenSuccessorIds = Arrays.stream(dataObject.getSuccessorIds()).boxed().collect(Collectors.toSet());
277       Set<Long> foundSuccessorIds = old.getSuccessors().stream().map(DataObject::getId).collect(Collectors.toSet());
278       if (!givenSuccessorIds.equals(foundSuccessorIds)) throw new InvalidBodyException(
279         "the given list of successors does not match the current list of successors"
280       );
281     }
282     dataObjectDAO.deleteAllAttributes(old);
283 
284     DataObject newParent = findRelatedDataObject(
285       old.getCollection().getShepardId(),
286       dataObject.getParentId(),
287       dataObjectShepardId
288     );
289     List<DataObject> newPredecessors = findRelatedDataObjects(
290       old.getCollection().getShepardId(),
291       dataObject.getPredecessorIds(),
292       dataObjectShepardId
293     );
294 
295     old.setShepardId(old.getShepardId());
296     old.setName(dataObject.getName());
297     old.setDescription(dataObject.getDescription());
298     old.setAttributes(dataObject.getAttributes());
299     old.setParent(newParent);
300     old.setPredecessors(newPredecessors);
301     old.setUpdatedAt(dateHelper.getDate());
302     old.setUpdatedBy(user);
303     DataObject updated = dataObjectDAO.createOrUpdate(old);
304     cutDeleted(updated);
305     return updated;
306   }
307 
308   /**
309    * set the deleted flag for the DataObject
310    *
311    * @param collectionShepardId ShepardId of the collection the dataobject is
312    *                            assigned to
313    * @param dataObjectShepardId identifies the DataObject to be deleted
314    * @return a boolean to identify if the DataObject was successfully removed
315    * @throws InvalidPathException if dataobject cannot be found or collection with
316    *                              collectionShepardId does not exist
317    * @throws InvalidAuthException if user does not have read or write permissions
318    *                              on the collection
319    */
320   public void deleteDataObject(long collectionShepardId, long dataObjectShepardId) {
321     getDataObject(collectionShepardId, dataObjectShepardId);
322     collectionService.assertIsAllowedToEditCollection(collectionShepardId);
323 
324     Date date = dateHelper.getDate();
325     User user = userService.getCurrentUser();
326 
327     if (!dataObjectDAO.deleteDataObjectByShepardId(dataObjectShepardId, user, date)) {
328       throw new InvalidRequestException("Could not delete DataObject with ShepardId %s".formatted(dataObjectShepardId));
329     }
330   }
331 
332   private DataObject cutDeleted(DataObject dataObject) {
333     var incoming = dataObject.getIncoming().stream().filter(i -> !i.isDeleted()).toList();
334     dataObject.setIncoming(incoming);
335     if (dataObject.getParent() != null && dataObject.getParent().isDeleted()) {
336       dataObject.setParent(null);
337     }
338     var children = dataObject.getChildren().stream().filter(s -> !s.isDeleted()).toList();
339     dataObject.setChildren(children);
340     var predecessors = dataObject.getPredecessors().stream().filter(s -> !s.isDeleted()).toList();
341     dataObject.setPredecessors(predecessors);
342     var successors = dataObject.getSuccessors().stream().filter(s -> !s.isDeleted()).toList();
343     dataObject.setSuccessors(successors);
344     var references = dataObject.getReferences().stream().filter(ref -> !ref.isDeleted()).toList();
345     dataObject.setReferences(references);
346     return dataObject;
347   }
348 
349   private List<DataObject> findRelatedDataObjects(
350     long collectionShepardId,
351     long[] referencedShepardIds,
352     Long dataObjectShepardId
353   ) {
354     if (referencedShepardIds == null) return new ArrayList<>();
355 
356     var result = new ArrayList<DataObject>(referencedShepardIds.length);
357     /*
358      * TODO: seems to be inefficient since this loops generates referencedIds.length
359      * calls to Neo4j this could possibly be packed into one query (or in chunks of
360      * queries in case of a large referencedIds array)
361      */
362     for (var shepardId : referencedShepardIds) {
363       result.add(findRelatedDataObject(collectionShepardId, shepardId, dataObjectShepardId));
364     }
365     return result;
366   }
367 
368   private DataObject findRelatedDataObject(
369     long collectionShepardId,
370     Long referencedShepardId,
371     Long dataObjectShepardId
372   ) {
373     if (referencedShepardId == null) return null;
374     else if (referencedShepardId.equals(dataObjectShepardId)) throw new InvalidBodyException(
375       "Self references are not allowed."
376     );
377 
378     var dataObject = dataObjectDAO.findByShepardId(referencedShepardId);
379     if (dataObject == null || dataObject.isDeleted()) throw new InvalidBodyException(
380       "The DataObject with id %d could not be found.".formatted(referencedShepardId)
381     );
382 
383     // Prevent cross collection references
384     if (!dataObject.getCollection().getShepardId().equals(collectionShepardId)) throw new InvalidBodyException(
385       "Related data objects must belong to the same collection as the new data object"
386     );
387 
388     return dataObject;
389   }
390 
391   /**
392    * Only needed for fixing session problems in unit tests
393    */
394   public void clearSession() {
395     dataObjectDAO.clearSession();
396   }
397 }