001package systems.dmx.core.impl;
002
003import systems.dmx.core.Association;
004import systems.dmx.core.AssociationType;
005import systems.dmx.core.DMXObject;
006import systems.dmx.core.Topic;
007import systems.dmx.core.TopicType;
008import systems.dmx.core.model.AssociationModel;
009import systems.dmx.core.model.RoleModel;
010import systems.dmx.core.model.SimpleValue;
011import systems.dmx.core.model.TopicModel;
012import systems.dmx.core.service.accesscontrol.AccessControlException;
013import systems.dmx.core.storage.spi.DMXStorage;
014
015import java.util.ArrayList;
016import java.util.Iterator;
017import java.util.List;
018import java.util.logging.Logger;
019
020
021
022/**
023 * Storage vendor agnostic access control on top of vendor specific storage.
024 *
025 * 2 kinds of methods:
026 *   - access controlled: get/create/update
027 *   - direct DB access: fetch/store (as derived from storage impl)
028 *
029 * ### TODO: no instatiations here
030 * ### TODO: hold storage object in instance variable (instead deriving) to make direct DB access more explicit
031 */
032public final class PersistenceLayer extends StorageDecorator {
033
034    // ------------------------------------------------------------------------------------------------------- Constants
035
036    private static final String URI_PREFIX_TOPIC_TYPE       = "domain.project.topic_type_";
037    private static final String URI_PREFIX_ASSOCIATION_TYPE = "domain.project.assoc_type_";
038    private static final String URI_PREFIX_ROLE_TYPE        = "domain.project.role_type_";
039
040    // ---------------------------------------------------------------------------------------------- Instance Variables
041
042    TypeStorage typeStorage;
043    EventManager em;
044    ModelFactoryImpl mf;
045
046    private final Logger logger = Logger.getLogger(getClass().getName());
047
048    // ---------------------------------------------------------------------------------------------------- Constructors
049
050    public PersistenceLayer(DMXStorage storage) {
051        super(storage);
052        // Note: mf must be initialzed before the type storage is instantiated
053        this.em = new EventManager();
054        this.mf = (ModelFactoryImpl) storage.getModelFactory();
055        this.typeStorage = new TypeStorage(this);
056        //
057        // Note: this is a constructor side effect. This is a cyclic dependency. This is nasty.
058        // ### TODO: explain why we do it.
059        mf.pl = this;
060        //
061        bootstrapTypeCache();
062    }
063
064    // ----------------------------------------------------------------------------------------- Package Private Methods
065
066
067
068    // === Topics ===
069
070    Topic getTopic(long topicId) {
071        try {
072            return checkReadAccessAndInstantiate(fetchTopic(topicId));
073        } catch (Exception e) {
074            throw new RuntimeException("Fetching topic " + topicId + " failed", e);
075        }
076    }
077
078    TopicImpl getTopicByUri(String uri) {
079        return getTopicByValue("uri", new SimpleValue(uri));
080    }
081
082    TopicImpl getTopicByValue(String key, SimpleValue value) {
083        try {
084            TopicModelImpl topic = fetchTopic(key, value);
085            return topic != null ? this.<TopicImpl>checkReadAccessAndInstantiate(topic) : null;
086            // Note: inside a conditional operator the type witness is required (at least in Java 6)
087        } catch (Exception e) {
088            throw new RuntimeException("Fetching topic failed (key=\"" + key + "\", value=\"" + value + "\")", e);
089        }
090    }
091
092    List<Topic> getTopicsByValue(String key, SimpleValue value) {
093        try {
094            return checkReadAccessAndInstantiate(fetchTopics(key, value));
095        } catch (Exception e) {
096            throw new RuntimeException("Fetching topics failed (key=\"" + key + "\", value=\"" + value + "\")", e);
097        }
098    }
099
100    List<Topic> getTopicsByType(String topicTypeUri) {
101        try {
102            return checkReadAccessAndInstantiate(_getTopicType(topicTypeUri).getAllInstances());
103        } catch (Exception e) {
104            throw new RuntimeException("Fetching topics by type failed (topicTypeUri=\"" + topicTypeUri + "\")", e);
105        }
106    }
107
108    List<Topic> searchTopics(String searchTerm, String fieldUri) {
109        try {
110            return checkReadAccessAndInstantiate(queryTopics(fieldUri, new SimpleValue(searchTerm)));
111        } catch (Exception e) {
112            throw new RuntimeException("Searching topics failed (searchTerm=\"" + searchTerm + "\", fieldUri=\"" +
113                fieldUri + "\")", e);
114        }
115    }
116
117    Iterable<Topic> getAllTopics() {
118        return new TopicIterable(this);
119    }
120
121    // ---
122
123    TopicImpl createTopic(TopicModelImpl model) {
124        try {
125            return updateValues(model, null).instantiate();
126        } catch (Exception e) {
127            throw new RuntimeException("Creating topic failed, model=" + model, e);
128        }
129    }
130
131    // ---
132
133    TopicImpl _createTopic(TopicModelImpl model) {
134        return _createTopic(model, null);   // uriPrefix=null
135    }
136
137    /**
138     * Creates a new topic in the DB.
139     * No child topics are created.
140     */
141    private TopicImpl _createTopic(TopicModelImpl model, String uriPrefix) {
142        try {
143            em.fireEvent(CoreEvent.PRE_CREATE_TOPIC, model);
144            //
145            model.preCreate();
146            //
147            // 1) store in DB
148            storeTopic(model);
149            if (model.getType().isSimple()) {
150                model.storeSimpleValue();
151            }
152            createTopicInstantiation(model.getId(), model.getTypeUri());
153            // 2) set default URI
154            // If no URI is given the topic gets a default URI based on its ID, if requested.
155            // Note: this must be done *after* the topic is stored. The ID is not known before.
156            // Note: in case no URI was given: once stored a topic's URI is empty (not null).
157            if (uriPrefix != null && model.getUri().equals("")) {
158                model.updateUri(uriPrefix + model.getId());     // update memory + DB
159            }
160            // 3) instantiate
161            TopicImpl topic = model.instantiate();
162            //
163            model.postCreate();
164            //
165            em.fireEvent(CoreEvent.POST_CREATE_TOPIC, topic);
166            return topic;
167        } catch (Exception e) {
168            throw new RuntimeException("Creating topic failed, model=" + model + ", uriPrefix=" + uriPrefix, e);
169        }
170    }
171
172    // ---
173
174    void updateTopic(TopicModelImpl updateModel) {
175        try {
176            TopicModelImpl model = fetchTopic(updateModel.getId());
177            updateTopic(model, updateModel);
178            //
179            // Note: POST_UPDATE_TOPIC_REQUEST is fired only once per update request.
180            // On the other hand TopicModel's update() method is called multiple times while updating the child topics
181            // (see ChildTopicsModelImpl).
182            em.fireEvent(CoreEvent.POST_UPDATE_TOPIC_REQUEST, model.instantiate());
183        } catch (Exception e) {
184            throw new RuntimeException("Fetching and updating topic " + updateModel.getId() + " failed", e);
185        }
186    }
187
188    void updateTopic(TopicModelImpl topic, TopicModelImpl updateModel) {
189        try {
190            topic.checkWriteAccess();
191            topic.update(updateModel);
192        } catch (Exception e) {
193            throw new RuntimeException("Updating topic " + topic.getId() + " failed", e);
194        }
195    }
196
197    // ---
198
199    /**
200     * Convenience.
201     */
202    void deleteTopic(long topicId) {
203        try {
204            deleteTopic(fetchTopic(topicId));
205        } catch (Exception e) {
206            throw new RuntimeException("Fetching and deleting topic " + topicId + " failed", e);
207        }
208    }
209
210    void deleteTopic(TopicModelImpl topic) {
211        try {
212            topic.checkWriteAccess();
213            topic.delete();
214        } catch (Exception e) {
215            throw new RuntimeException("Deleting topic " + topic.getId() + " failed", e);
216        }
217    }
218
219
220
221    // === Associations ===
222
223    Association getAssociation(long assocId) {
224        try {
225            return checkReadAccessAndInstantiate(fetchAssociation(assocId));
226        } catch (Exception e) {
227            throw new RuntimeException("Fetching association " + assocId + " failed", e);
228        }
229    }
230
231    Association getAssociationByValue(String key, SimpleValue value) {
232        try {
233            AssociationModelImpl assoc = fetchAssociation(key, value);
234            return assoc != null ? this.<Association>checkReadAccessAndInstantiate(assoc) : null;
235            // Note: inside a conditional operator the type witness is required (at least in Java 6)
236        } catch (Exception e) {
237            throw new RuntimeException("Fetching association failed (key=\"" + key + "\", value=\"" + value + "\")", e);
238        }
239    }
240
241    List<Association> getAssociationsByValue(String key, SimpleValue value) {
242        try {
243            return checkReadAccessAndInstantiate(fetchAssociations(key, value));
244        } catch (Exception e) {
245            throw new RuntimeException("Fetching associationss failed (key=\"" + key + "\", value=\"" + value + "\")",
246                e);
247        }
248    }
249
250    Association getAssociation(String assocTypeUri, long topic1Id, long topic2Id, String roleTypeUri1,
251                                                                                  String roleTypeUri2) {
252        String info = "assocTypeUri=\"" + assocTypeUri + "\", topic1Id=" + topic1Id + ", topic2Id=" + topic2Id +
253            ", roleTypeUri1=\"" + roleTypeUri1 + "\", roleTypeUri2=\"" + roleTypeUri2 + "\"";
254        try {
255            AssociationModelImpl assoc = fetchAssociation(assocTypeUri, topic1Id, topic2Id, roleTypeUri1, roleTypeUri2);
256            return assoc != null ? this.<Association>checkReadAccessAndInstantiate(assoc) : null;
257            // Note: inside a conditional operator the type witness is required (at least in Java 6)
258        } catch (Exception e) {
259            throw new RuntimeException("Fetching association failed (" + info + ")", e);
260        }
261    }
262
263    Association getAssociationBetweenTopicAndAssociation(String assocTypeUri, long topicId, long assocId,
264                                                         String topicRoleTypeUri, String assocRoleTypeUri) {
265        String info = "assocTypeUri=\"" + assocTypeUri + "\", topicId=" + topicId + ", assocId=" + assocId +
266            ", topicRoleTypeUri=\"" + topicRoleTypeUri + "\", assocRoleTypeUri=\"" + assocRoleTypeUri + "\"";
267        logger.info(info);
268        try {
269            AssociationModelImpl assoc = fetchAssociationBetweenTopicAndAssociation(assocTypeUri, topicId, assocId,
270                topicRoleTypeUri, assocRoleTypeUri);
271            return assoc != null ? this.<Association>checkReadAccessAndInstantiate(assoc) : null;
272            // Note: inside a conditional operator the type witness is required (at least in Java 6)
273        } catch (Exception e) {
274            throw new RuntimeException("Fetching association failed (" + info + ")", e);
275        }
276    }
277
278    // ---
279
280    List<Association> getAssociationsByType(String assocTypeUri) {
281        try {
282            return checkReadAccessAndInstantiate(_getAssociationType(assocTypeUri).getAllInstances());
283        } catch (Exception e) {
284            throw new RuntimeException("Fetching associations by type failed (assocTypeUri=\"" + assocTypeUri + "\")",
285                e);
286        }
287    }
288
289    List<Association> getAssociations(long topic1Id, long topic2Id) {
290        return getAssociations(null, topic1Id, topic2Id);   // assocTypeUri=null
291    }
292
293    List<Association> getAssociations(String assocTypeUri, long topic1Id, long topic2Id) {
294        return getAssociations(assocTypeUri, topic1Id, topic2Id, null, null);   // roleTypeUri1=null, roleTypeUri2=null
295    }
296
297    List<Association> getAssociations(String assocTypeUri, long topic1Id, long topic2Id, String roleTypeUri1,
298                                                                                         String roleTypeUri2) {
299        return instantiate(_getAssociations(assocTypeUri, topic1Id, topic2Id, roleTypeUri1, roleTypeUri2));
300    }
301
302    /**
303     * Fetches from DB and filters READables. No instantiation.
304     *
305     * ### TODO: drop this. Use the new traversal methods instead.
306     */
307    Iterable<AssociationModelImpl> _getAssociations(String assocTypeUri, long topic1Id, long topic2Id,
308                                                    String roleTypeUri1, String roleTypeUri2) {
309        logger.fine("assocTypeUri=\"" + assocTypeUri + "\", topic1Id=" + topic1Id + ", topic2Id=" + topic2Id +
310            ", roleTypeUri1=\"" + roleTypeUri1 + "\", roleTypeUri2=\"" + roleTypeUri2 + "\"");
311        try {
312            return filterReadables(fetchAssociations(assocTypeUri, topic1Id, topic2Id, roleTypeUri1, roleTypeUri2));
313        } catch (Exception e) {
314            throw new RuntimeException("Fetching associations between topics " + topic1Id + " and " + topic2Id +
315                " failed (assocTypeUri=\"" + assocTypeUri + "\", roleTypeUri1=\"" + roleTypeUri1 +
316                "\", roleTypeUri2=\"" + roleTypeUri2 + "\")", e);
317        }
318    }
319
320    // ---
321
322    Iterable<Association> getAllAssociations() {
323        return new AssociationIterable(this);
324    }
325
326    long[] getPlayerIds(long assocId) {
327        return fetchPlayerIds(assocId);
328    }
329
330    // ---
331
332    /**
333     * Convenience.
334     */
335    AssociationImpl createAssociation(String typeUri, RoleModel roleModel1, RoleModel roleModel2) {
336        return createAssociation(mf.newAssociationModel(typeUri, roleModel1, roleModel2));
337    }
338
339    /**
340     * Creates a new association in the DB.
341     */
342    AssociationImpl createAssociation(AssociationModelImpl model) {
343        try {
344            em.fireEvent(CoreEvent.PRE_CREATE_ASSOCIATION, model);
345            //
346            model.preCreate();
347            //
348            // 1) store in DB
349            storeAssociation(model);
350            updateValues(model, null);
351            createAssociationInstantiation(model.getId(), model.getTypeUri());
352            // 2) instantiate
353            AssociationImpl assoc = model.instantiate();
354            //
355            model.postCreate();
356            //
357            em.fireEvent(CoreEvent.POST_CREATE_ASSOCIATION, assoc);
358            return assoc;
359        } catch (Exception e) {
360            throw new RuntimeException("Creating association failed, model=" + model, e);
361        }
362    }
363
364    // ---
365
366    void updateAssociation(AssociationModelImpl updateModel) {
367        try {
368            AssociationModelImpl model = fetchAssociation(updateModel.getId());
369            updateAssociation(model, updateModel);
370            //
371            // Note: there is no possible POST_UPDATE_ASSOCIATION_REQUEST event to fire here (compare to updateTopic()).
372            // It would be equivalent to POST_UPDATE_ASSOCIATION. Per request exactly one association is updated.
373            // Its childs are always topics (never associations).
374        } catch (Exception e) {
375            throw new RuntimeException("Fetching and updating association " + updateModel.getId() + " failed", e);
376        }
377    }
378
379    void updateAssociation(AssociationModelImpl assoc, AssociationModelImpl updateModel) {
380        try {
381            checkAssociationWriteAccess(assoc.getId());
382            assoc.update(updateModel);
383        } catch (Exception e) {
384            throw new RuntimeException("Updating association " + assoc.getId() + " failed, assoc=" + assoc +
385                ", updateModel=" + updateModel, e);
386        }
387    }
388
389    // ---
390
391    /**
392     * Convenience.
393     */
394    void deleteAssociation(long assocId) {
395        try {
396            deleteAssociation(fetchAssociation(assocId));
397        } catch (IllegalStateException e) {
398            // Note: fetchAssociation() might throw IllegalStateException and is no problem.
399            // This happens when the association is deleted already. In this case nothing needs to be performed.
400            //
401            // Compare to DMXObjectModelImpl.delete()
402            // TODO: introduce storage-vendor neutral DM exception.
403            if (e.getMessage().equals("Node[" + assocId + "] has been deleted in this tx")) {
404                logger.info("### Association " + assocId + " has already been deleted in this transaction. " +
405                    "This can happen while delete-multi.");
406            } else {
407                throw e;
408            }
409        } catch (Exception e) {
410            throw new RuntimeException("Fetching and deleting association " + assocId + " failed", e);
411        }
412    }
413
414    void deleteAssociation(AssociationModelImpl assoc) {
415        try {
416            checkAssociationWriteAccess(assoc.getId());
417            assoc.delete();
418        } catch (Exception e) {
419            throw new RuntimeException("Deleting association " + assoc.getId() + " failed", e);
420        }
421    }
422
423
424
425    // ===
426
427    void createTopicInstantiation(long topicId, String topicTypeUri) {
428        try {
429            AssociationModel assoc = mf.newAssociationModel("dmx.core.instantiation",
430                mf.newTopicRoleModel(topicTypeUri, "dmx.core.type"),
431                mf.newTopicRoleModel(topicId, "dmx.core.instance"));
432            storeAssociation(assoc);   // direct storage calls used here ### explain
433            storeAssociationValue(assoc.getId(), assoc.getSimpleValue());
434            createAssociationInstantiation(assoc.getId(), assoc.getTypeUri());
435        } catch (Exception e) {
436            throw new RuntimeException("Associating topic " + topicId + " with topic type \"" +
437                topicTypeUri + "\" failed", e);
438        }
439    }
440
441    void createAssociationInstantiation(long assocId, String assocTypeUri) {
442        try {
443            AssociationModel assoc = mf.newAssociationModel("dmx.core.instantiation",
444                mf.newTopicRoleModel(assocTypeUri, "dmx.core.type"),
445                mf.newAssociationRoleModel(assocId, "dmx.core.instance"));
446            storeAssociation(assoc);   // direct storage calls used here ### explain
447            storeAssociationValue(assoc.getId(), assoc.getSimpleValue());
448        } catch (Exception e) {
449            throw new RuntimeException("Associating association " + assocId + " with association type \"" +
450                assocTypeUri + "\" failed", e);
451        }
452    }
453
454
455
456    // === Types ===
457
458    TopicTypeImpl getTopicType(String uri) {
459        return checkReadAccessAndInstantiate(_getTopicType(uri));
460    }
461
462    TopicTypeImpl getTopicTypeImplicitly(long topicId) {
463        checkTopicReadAccess(topicId);
464        return _getTopicType(typeUri(topicId)).instantiate();
465    }
466
467    // ---
468
469    AssociationTypeImpl getAssociationType(String uri) {
470        return checkReadAccessAndInstantiate(_getAssociationType(uri));
471    }
472
473    AssociationTypeImpl getAssociationTypeImplicitly(long assocId) {
474        checkAssociationReadAccess(assocId);
475        return _getAssociationType(typeUri(assocId)).instantiate();
476    }
477
478    // ---
479
480    List<TopicType> getAllTopicTypes() {
481        try {
482            List<TopicType> topicTypes = new ArrayList();
483            for (String uri : getTopicTypeUris()) {
484                topicTypes.add(_getTopicType(uri).instantiate());
485            }
486            return topicTypes;
487        } catch (Exception e) {
488            throw new RuntimeException("Fetching all topic types failed", e);
489        }
490    }
491
492    List<AssociationType> getAllAssociationTypes() {
493        try {
494            List<AssociationType> assocTypes = new ArrayList();
495            for (String uri : getAssociationTypeUris()) {
496                assocTypes.add(_getAssociationType(uri).instantiate());
497            }
498            return assocTypes;
499        } catch (Exception e) {
500            throw new RuntimeException("Fetching all association types failed", e);
501        }
502    }
503
504    // ---
505
506    TopicTypeImpl createTopicType(TopicTypeModelImpl model) {
507        try {
508            em.fireEvent(CoreEvent.PRE_CREATE_TOPIC_TYPE, model);
509            //
510            // store in DB
511            createType(model, URI_PREFIX_TOPIC_TYPE);
512            //
513            TopicTypeImpl topicType = model.instantiate();
514            em.fireEvent(CoreEvent.INTRODUCE_TOPIC_TYPE, topicType);
515            //
516            return topicType;
517        } catch (Exception e) {
518            throw new RuntimeException("Creating topic type \"" + model.getUri() + "\" failed", e);
519        }
520    }
521
522    AssociationTypeImpl createAssociationType(AssociationTypeModelImpl model) {
523        try {
524            em.fireEvent(CoreEvent.PRE_CREATE_ASSOCIATION_TYPE, model);
525            //
526            // store in DB
527            createType(model, URI_PREFIX_ASSOCIATION_TYPE);
528            //
529            AssociationTypeImpl assocType = model.instantiate();
530            em.fireEvent(CoreEvent.INTRODUCE_ASSOCIATION_TYPE, assocType);
531            //
532            return assocType;
533        } catch (Exception e) {
534            throw new RuntimeException("Creating association type \"" + model.getUri() + "\" failed", e);
535        }
536    }
537
538    // ---
539
540    void updateTopicType(TopicTypeModelImpl updateModel) {
541        try {
542            // Note: type lookup is by ID. The URI might have changed, the ID does not.
543            TopicModelImpl topic = fetchTopic(updateModel.getId());
544            topic.checkWriteAccess();
545            _getTopicType(topic.getUri()).update(updateModel);
546        } catch (Exception e) {
547            throw new RuntimeException("Updating topic type failed, updateModel=" + updateModel, e);
548        }
549    }
550
551    void updateAssociationType(AssociationTypeModelImpl updateModel) {
552        try {
553            // Note: type lookup is by ID. The URI might have changed, the ID does not.
554            TopicModelImpl topic = fetchTopic(updateModel.getId());
555            topic.checkWriteAccess();
556            _getAssociationType(topic.getUri()).update(updateModel);
557        } catch (Exception e) {
558            throw new RuntimeException("Updating association type failed, updateModel=" + updateModel, e);
559        }
560    }
561
562    // ---
563
564    void deleteTopicType(String topicTypeUri) {
565        try {
566            TypeModelImpl type = _getTopicType(topicTypeUri);
567            type.checkWriteAccess();
568            type.delete();
569            // ### TODO: delete view config topics
570        } catch (Exception e) {
571            throw new RuntimeException("Deleting topic type \"" + topicTypeUri + "\" failed", e);
572        }
573    }
574
575    void deleteAssociationType(String assocTypeUri) {
576        try {
577            TypeModelImpl type = _getAssociationType(assocTypeUri);
578            type.checkWriteAccess();
579            type.delete();
580            // ### TODO: delete view config topics
581        } catch (Exception e) {
582            throw new RuntimeException("Deleting association type \"" + assocTypeUri + "\" failed", e);
583        }
584    }
585
586    // ---
587
588    Topic createRoleType(TopicModelImpl model) {
589        // check type URI argument
590        String typeUri = model.getTypeUri();
591        if (typeUri == null) {
592            model.setTypeUri("dmx.core.role_type");
593        } else {
594            if (!typeUri.equals("dmx.core.role_type")) {
595                throw new IllegalArgumentException("A role type is supposed to be of type \"dmx.core.role_type\" " +
596                    "(found: \"" + typeUri + "\")");
597            }
598        }
599        //
600        return _createTopic(model, URI_PREFIX_ROLE_TYPE);
601    }
602
603    // ---
604
605    /**
606     * Type cache direct access. No permission check.
607     */
608    TopicTypeModelImpl _getTopicType(String uri) {
609        return typeStorage.getTopicType(uri);
610    }
611
612    /**
613     * Type cache direct access. No permission check.
614     */
615    AssociationTypeModelImpl _getAssociationType(String uri) {
616        return typeStorage.getAssociationType(uri);
617    }
618
619
620
621    // === Generic Object ===
622
623    DMXObject getObject(long id) {
624        return checkReadAccessAndInstantiate(fetchObject(id));
625    }
626
627
628
629    // === Traversal ===
630
631    // --- Topic Source ---
632
633    List<RelatedTopicModelImpl> getTopicRelatedTopics(long topicId, String assocTypeUri, String myRoleTypeUri,
634                                                      String othersRoleTypeUri, String othersTopicTypeUri) {
635        return filterReadables(fetchTopicRelatedTopics(topicId, assocTypeUri, myRoleTypeUri, othersRoleTypeUri,
636            othersTopicTypeUri));
637    }
638
639    List<RelatedTopicModelImpl> getTopicRelatedTopics(long topicId, List<String> assocTypeUris, String myRoleTypeUri,
640                                                      String othersRoleTypeUri, String othersTopicTypeUri) {
641        return filterReadables(fetchTopicRelatedTopics(topicId, assocTypeUris, myRoleTypeUri, othersRoleTypeUri,
642            othersTopicTypeUri));
643    }
644
645    RelatedAssociationModelImpl getTopicRelatedAssociation(long topicId, String assocTypeUri, String myRoleTypeUri,
646                                                           String othersRoleTypeUri, String othersAssocTypeUri) {
647        RelatedAssociationModelImpl assoc = fetchTopicRelatedAssociation(topicId, assocTypeUri, myRoleTypeUri,
648            othersRoleTypeUri, othersAssocTypeUri);
649        return assoc != null ? checkReadAccess(assoc) : null;
650    }
651
652    List<RelatedAssociationModelImpl> getTopicRelatedAssociations(long topicId, String assocTypeUri,
653                                            String myRoleTypeUri, String othersRoleTypeUri, String othersAssocTypeUri) {
654        return filterReadables(fetchTopicRelatedAssociations(topicId, assocTypeUri, myRoleTypeUri, othersRoleTypeUri,
655            othersAssocTypeUri));
656    }
657
658    List<AssociationModelImpl> getTopicAssociations(long topicId) {
659        return filterReadables(fetchTopicAssociations(topicId));
660    }
661
662    // --- Association Source ---
663
664    List<RelatedTopicModelImpl> getAssociationRelatedTopics(long assocId, String assocTypeUri,
665                                            String myRoleTypeUri, String othersRoleTypeUri, String othersTopicTypeUri) {
666        return filterReadables(fetchAssociationRelatedTopics(assocId, assocTypeUri, myRoleTypeUri, othersRoleTypeUri,
667            othersTopicTypeUri));
668    }
669
670    List<RelatedTopicModelImpl> getAssociationRelatedTopics(long assocId, List<String> assocTypeUris,
671                                            String myRoleTypeUri, String othersRoleTypeUri, String othersTopicTypeUri) {
672        return filterReadables(fetchAssociationRelatedTopics(assocId, assocTypeUris, myRoleTypeUri, othersRoleTypeUri,
673            othersTopicTypeUri));
674    }
675
676    RelatedAssociationModelImpl getAssociationRelatedAssociation(long assocId, String assocTypeUri,
677                                            String myRoleTypeUri, String othersRoleTypeUri, String othersAssocTypeUri) {
678        RelatedAssociationModelImpl assoc = fetchAssociationRelatedAssociation(assocId, assocTypeUri, myRoleTypeUri,
679            othersRoleTypeUri, othersAssocTypeUri);
680        return assoc != null ? checkReadAccess(assoc) : null;
681    }
682
683    List<RelatedAssociationModelImpl> getAssociationRelatedAssociations(long assocId, String assocTypeUri,
684                                            String myRoleTypeUri, String othersRoleTypeUri, String othersAssocTypeUri) {
685        return filterReadables(fetchAssociationRelatedAssociations(assocId, assocTypeUri, myRoleTypeUri,
686            othersRoleTypeUri, othersAssocTypeUri));
687    }
688
689    List<AssociationModelImpl> getAssociationAssociations(long assocId) {
690        return filterReadables(fetchAssociationAssociations(assocId));
691    }
692
693    // --- Object Source ---
694
695    RelatedTopicModelImpl getRelatedTopic(long objectId, String assocTypeUri, String myRoleTypeUri,
696                                          String othersRoleTypeUri, String othersTopicTypeUri) {
697        RelatedTopicModelImpl topic = fetchRelatedTopic(objectId, assocTypeUri, myRoleTypeUri, othersRoleTypeUri,
698            othersTopicTypeUri);
699        return topic != null ? checkReadAccess(topic) : null;
700    }
701
702    List<RelatedTopicModelImpl> getRelatedTopics(long objectId, String assocTypeUri, String myRoleTypeUri,
703                                                 String othersRoleTypeUri, String othersTopicTypeUri) {
704        return filterReadables(fetchRelatedTopics(objectId, assocTypeUri, myRoleTypeUri, othersRoleTypeUri,
705            othersTopicTypeUri));
706    }
707
708
709
710    // === Properties ===
711
712    List<Topic> getTopicsByProperty(String propUri, Object propValue) {
713        return checkReadAccessAndInstantiate(fetchTopicsByProperty(propUri, propValue));
714    }
715
716    List<Topic> getTopicsByPropertyRange(String propUri, Number from, Number to) {
717        return checkReadAccessAndInstantiate(fetchTopicsByPropertyRange(propUri, from, to));
718    }
719
720    List<Association> getAssociationsByProperty(String propUri, Object propValue) {
721        return checkReadAccessAndInstantiate(fetchAssociationsByProperty(propUri, propValue));
722    }
723
724    List<Association> getAssociationsByPropertyRange(String propUri, Number from, Number to) {
725        return checkReadAccessAndInstantiate(fetchAssociationsByPropertyRange(propUri, from, to));
726    }
727
728    // ------------------------------------------------------------------------------------------------- Private Methods
729
730
731
732    // === Access Control / Instantiation ===
733
734    // These methods 1) instantiate objects from models, and 2) check the READ permission for each model.
735    // Call these methods when passing objects fetched from the DB to the user.
736
737    // ### TODO: drop this. No instatiations in this class.
738    <O> O checkReadAccessAndInstantiate(DMXObjectModelImpl model) {
739        return (O) checkReadAccess(model).instantiate();
740    }
741
742    // ### TODO: drop this. No instatiations in this class.
743    <O> List<O> checkReadAccessAndInstantiate(List<? extends DMXObjectModelImpl> models) {
744        return instantiate(filterReadables(models));
745    }
746
747    // ---
748
749    private <M extends DMXObjectModelImpl> List<M> filterReadables(List<M> models) {
750        Iterator<? extends DMXObjectModelImpl> i = models.iterator();
751        while (i.hasNext()) {
752            if (!hasReadAccess(i.next())) {
753                i.remove();
754            }
755        }
756        return models;
757    }
758
759    boolean hasReadAccess(DMXObjectModelImpl model) {
760        try {
761            checkReadAccess(model);
762            return true;
763        } catch (AccessControlException e) {
764            return false;
765        }
766    }
767
768    // ---
769
770    // TODO: add return to model's checkReadAccess() and drop this method?
771    <M extends DMXObjectModelImpl> M checkReadAccess(M model) {
772        model.checkReadAccess();
773        return model;
774    }
775
776    // ---
777
778    /**
779     * @throws  AccessControlException  if the current user has no permission.
780     */
781    void checkTopicReadAccess(long topicId) {
782        em.fireEvent(CoreEvent.CHECK_TOPIC_READ_ACCESS, topicId);
783    }
784
785    /**
786     * @throws  AccessControlException  if the current user has no permission.
787     */
788    void checkAssociationReadAccess(long assocId) {
789        em.fireEvent(CoreEvent.CHECK_ASSOCIATION_READ_ACCESS, assocId);
790    }
791
792    // ---
793
794    /**
795     * @throws  AccessControlException  if the current user has no permission.
796     */
797    void checkTopicWriteAccess(long topicId) {
798        em.fireEvent(CoreEvent.CHECK_TOPIC_WRITE_ACCESS, topicId);
799    }
800
801    /**
802     * @throws  AccessControlException  if the current user has no permission.
803     */
804    void checkAssociationWriteAccess(long assocId) {
805        em.fireEvent(CoreEvent.CHECK_ASSOCIATION_WRITE_ACCESS, assocId);
806    }
807
808
809
810    // === Instantiation ===
811
812    // ### TODO: move to kernel utils
813    <O> List<O> instantiate(Iterable<? extends DMXObjectModelImpl> models) {
814        List<O> objects = new ArrayList();
815        for (DMXObjectModelImpl model : models) {
816            objects.add((O) model.instantiate());
817        }
818        return objects;
819    }
820
821
822
823    // ===
824
825    private List<String> getTopicTypeUris() {
826        try {
827            List<String> topicTypeUris = new ArrayList();
828            // add meta types
829            topicTypeUris.add("dmx.core.topic_type");
830            topicTypeUris.add("dmx.core.assoc_type");
831            topicTypeUris.add("dmx.core.meta_type");
832            // add regular types
833            for (TopicModel topicType : filterReadables(fetchTopics("typeUri", new SimpleValue(
834                                                                    "dmx.core.topic_type")))) {
835                topicTypeUris.add(topicType.getUri());
836            }
837            return topicTypeUris;
838        } catch (Exception e) {
839            throw new RuntimeException("Fetching list of topic type URIs failed", e);
840        }
841    }
842
843    private List<String> getAssociationTypeUris() {
844        try {
845            List<String> assocTypeUris = new ArrayList();
846            for (TopicModel assocType : filterReadables(fetchTopics("typeUri", new SimpleValue(
847                                                                    "dmx.core.assoc_type")))) {
848                assocTypeUris.add(assocType.getUri());
849            }
850            return assocTypeUris;
851        } catch (Exception e) {
852            throw new RuntimeException("Fetching list of association type URIs failed", e);
853        }
854    }
855
856    // ---
857
858    private void createType(TypeModelImpl model, String uriPrefix) {
859        // Note: the type topic is instantiated explicitly on a `TopicModel` (which is freshly created from the
860        // `TypeModel`). Creating the type topic from the `TypeModel` directly would fail as topic creation implies
861        // topic instantiation, and due to the polymorphic `instantiate()` method a `Type` object would be instantiated
862        // (instead a `Topic` object). But instantiating a type implies per-user type projection, that is removing the
863        // assoc defs not readable by the current user. But at the time the type topic is stored in the DB its assoc
864        // defs are not yet stored, and the readability check would fail.
865        TopicModelImpl typeTopic = mf.newTopicModel(model);
866        _createTopic(typeTopic, uriPrefix);     // create generic topic
867        //
868        model.id = typeTopic.id;
869        model.uri = typeTopic.uri;
870        //
871        typeStorage.storeType(model);           // store type-specific parts
872    }
873
874    private String typeUri(long objectId) {
875        return (String) fetchProperty(objectId, "typeUri");
876    }
877
878    private void bootstrapTypeCache() {
879        TopicTypeModelImpl metaMetaType = mf.newTopicTypeModel("dmx.core.meta_meta_type", "Meta Meta Type",
880            "dmx.core.text");
881        metaMetaType.setTypeUri("dmx.core.meta_meta_meta_type");
882        typeStorage.putInTypeCache(metaMetaType);
883    }
884
885    // ---
886
887    private <M extends DMXObjectModelImpl> M updateValues(M updateModel, M targetObject) {
888        M value = new ValueIntegrator(this).integrate(updateModel, targetObject, null).value;
889        // sanity check
890        if (value == null) {
891            throw new RuntimeException("ValueIntegrator yields no result");
892        }
893        //
894        return value;
895    }
896}