001package de.deepamehta.core.impl;
002
003import de.deepamehta.core.Association;
004import de.deepamehta.core.AssociationType;
005import de.deepamehta.core.DeepaMehtaObject;
006import de.deepamehta.core.RelatedAssociation;
007import de.deepamehta.core.RelatedTopic;
008import de.deepamehta.core.Topic;
009import de.deepamehta.core.TopicType;
010import de.deepamehta.core.model.AssociationModel;
011import de.deepamehta.core.model.AssociationTypeModel;
012import de.deepamehta.core.model.DeepaMehtaObjectModel;
013import de.deepamehta.core.model.RelatedAssociationModel;
014import de.deepamehta.core.model.RelatedTopicModel;
015import de.deepamehta.core.model.RoleModel;
016import de.deepamehta.core.model.SimpleValue;
017import de.deepamehta.core.model.TopicModel;
018import de.deepamehta.core.model.TopicTypeModel;
019import de.deepamehta.core.service.accesscontrol.AccessControlException;
020import de.deepamehta.core.storage.spi.DeepaMehtaStorage;
021
022import java.util.ArrayList;
023import java.util.Collection;
024import java.util.Iterator;
025import java.util.List;
026import java.util.logging.Logger;
027
028
029
030public class PersistenceLayer extends StorageDecorator {
031
032    // ------------------------------------------------------------------------------------------------------- Constants
033
034    private static final String URI_PREFIX_TOPIC_TYPE       = "domain.project.topic_type_";
035    private static final String URI_PREFIX_ASSOCIATION_TYPE = "domain.project.assoc_type_";
036    private static final String URI_PREFIX_ROLE_TYPE        = "domain.project.role_type_";
037
038    // ---------------------------------------------------------------------------------------------- Instance Variables
039
040    TypeStorage typeStorage;
041    ValueStorage valueStorage;
042
043    EventManager em;
044    ModelFactoryImpl mf;
045
046    private final Logger logger = Logger.getLogger(getClass().getName());
047
048    // ---------------------------------------------------------------------------------------------------- Constructors
049
050    public PersistenceLayer(DeepaMehtaStorage 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        //
056        this.typeStorage = new TypeStorage(this);
057        this.valueStorage = new ValueStorage(this);
058        //
059        // Note: this is a constructor side effect. This is a cyclic dependency. This is nasty.
060        // ### TODO: explain why we do it.
061        mf.pl = this;
062        //
063        bootstrapTypeCache();
064    }
065
066    // ----------------------------------------------------------------------------------------- Package Private Methods
067
068
069
070    // === Topics ===
071
072    Topic getTopic(long topicId) {
073        try {
074            return checkReadAccessAndInstantiate(fetchTopic(topicId));
075        } catch (Exception e) {
076            throw new RuntimeException("Fetching topic " + topicId + " failed", e);
077        }
078    }
079
080    TopicImpl getTopicByUri(String uri) {
081        return getTopicByValue("uri", new SimpleValue(uri));
082    }
083
084    TopicImpl getTopicByValue(String key, SimpleValue value) {
085        try {
086            TopicModelImpl topic = fetchTopic(key, value);
087            return topic != null ? this.<TopicImpl>checkReadAccessAndInstantiate(topic) : null;
088            // Note: inside a conditional operator the type witness is required (at least in Java 6)
089        } catch (Exception e) {
090            throw new RuntimeException("Fetching topic failed (key=\"" + key + "\", value=\"" + value + "\")", e);
091        }
092    }
093
094    List<Topic> getTopicsByValue(String key, SimpleValue value) {
095        try {
096            return checkReadAccessAndInstantiate(fetchTopics(key, value));
097        } catch (Exception e) {
098            throw new RuntimeException("Fetching topics failed (key=\"" + key + "\", value=\"" + value + "\")", e);
099        }
100    }
101
102    List<Topic> getTopicsByType(String topicTypeUri) {
103        try {
104            return checkReadAccessAndInstantiate(_getTopicType(topicTypeUri).getAllInstances());
105        } catch (Exception e) {
106            throw new RuntimeException("Fetching topics by type failed (topicTypeUri=\"" + topicTypeUri + "\")", e);
107        }
108    }
109
110    List<Topic> searchTopics(String searchTerm, String fieldUri) {
111        try {
112            return checkReadAccessAndInstantiate(queryTopics(fieldUri, new SimpleValue(searchTerm)));
113        } catch (Exception e) {
114            throw new RuntimeException("Searching topics failed (searchTerm=\"" + searchTerm + "\", fieldUri=\"" +
115                fieldUri + "\")", e);
116        }
117    }
118
119    Iterable<Topic> getAllTopics() {
120        return new TopicIterable(this);
121    }
122
123    // ---
124
125    /**
126     * Convenience.
127     */
128    TopicImpl createTopic(TopicModel model) {
129        return createTopic(model, null);    // uriPrefix=null
130    }
131
132    /**
133     * Creates a new topic in the DB.
134     */
135    TopicImpl createTopic(TopicModel model, String uriPrefix) {
136        try {
137            em.fireEvent(CoreEvent.PRE_CREATE_TOPIC, model);
138            //
139            // 1) store in DB
140            storeTopic(model);
141            valueStorage.storeValue((TopicModelImpl) model);
142            createTopicInstantiation(model.getId(), model.getTypeUri());
143            // 2) set default URI
144            // If no URI is given the topic gets a default URI based on its ID, if requested.
145            // Note: this must be done *after* the topic is stored. The ID is not known before.
146            // Note: in case no URI was given: once stored a topic's URI is empty (not null).
147            if (uriPrefix != null && model.getUri().equals("")) {
148                ((TopicModelImpl) model).updateUri(uriPrefix + model.getId());
149            }
150            // 3) instantiate
151            TopicImpl topic = new TopicImpl((TopicModelImpl) model, this);
152            //
153            em.fireEvent(CoreEvent.POST_CREATE_TOPIC, topic);
154            return topic;
155        } catch (Exception e) {
156            throw new RuntimeException("Creating topic " + model.getId() + " failed (typeUri=\"" + model.getTypeUri() +
157                "\")", e);
158        }
159    }
160
161    // ---
162
163    void updateTopic(TopicModel newModel) {
164        long topicId = newModel.getId();
165        try {
166            checkTopicWriteAccess(topicId);
167            //
168            TopicModelImpl model = fetchTopic(topicId);
169            model.update(newModel);
170            //
171            // Note: POST_UPDATE_TOPIC_REQUEST is fired only once per update request.
172            // On the other hand TopicModel's update() method is called multiple times while updating the child topics
173            // (see ChildTopicsModelImpl).
174            em.fireEvent(CoreEvent.POST_UPDATE_TOPIC_REQUEST, model.instantiate());
175        } catch (Exception e) {
176            throw new RuntimeException("Updating topic " + topicId + " failed", e);
177        }
178    }
179
180    void deleteTopic(long topicId) {
181        try {
182            checkTopicWriteAccess(topicId);
183            //
184            fetchTopic(topicId).delete();
185        } catch (Exception e) {
186            throw new RuntimeException("Deleting topic " + topicId + " failed", e);
187        }
188    }
189
190
191
192    // === Associations ===
193
194    Association getAssociation(long assocId) {
195        try {
196            return checkReadAccessAndInstantiate(fetchAssociation(assocId));
197        } catch (Exception e) {
198            throw new RuntimeException("Fetching association " + assocId + " failed", e);
199        }
200    }
201
202    Association getAssociationByValue(String key, SimpleValue value) {
203        try {
204            AssociationModelImpl assoc = fetchAssociation(key, value);
205            return assoc != null ? this.<Association>checkReadAccessAndInstantiate(assoc) : null;
206            // Note: inside a conditional operator the type witness is required (at least in Java 6)
207        } catch (Exception e) {
208            throw new RuntimeException("Fetching association failed (key=\"" + key + "\", value=\"" + value + "\")", e);
209        }
210    }
211
212    List<Association> getAssociationsByValue(String key, SimpleValue value) {
213        try {
214            return checkReadAccessAndInstantiate(fetchAssociations(key, value));
215        } catch (Exception e) {
216            throw new RuntimeException("Fetching associationss failed (key=\"" + key + "\", value=\"" + value + "\")",
217                e);
218        }
219    }
220
221    Association getAssociation(String assocTypeUri, long topic1Id, long topic2Id, String roleTypeUri1,
222                                                                                  String roleTypeUri2) {
223        String info = "assocTypeUri=\"" + assocTypeUri + "\", topic1Id=" + topic1Id + ", topic2Id=" + topic2Id +
224            ", roleTypeUri1=\"" + roleTypeUri1 + "\", roleTypeUri2=\"" + roleTypeUri2 + "\"";
225        try {
226            AssociationModelImpl assoc = fetchAssociation(assocTypeUri, topic1Id, topic2Id, roleTypeUri1, roleTypeUri2);
227            return assoc != null ? this.<Association>checkReadAccessAndInstantiate(assoc) : null;
228            // Note: inside a conditional operator the type witness is required (at least in Java 6)
229        } catch (Exception e) {
230            throw new RuntimeException("Fetching association failed (" + info + ")", e);
231        }
232    }
233
234    Association getAssociationBetweenTopicAndAssociation(String assocTypeUri, long topicId, long assocId,
235                                                         String topicRoleTypeUri, String assocRoleTypeUri) {
236        String info = "assocTypeUri=\"" + assocTypeUri + "\", topicId=" + topicId + ", assocId=" + assocId +
237            ", topicRoleTypeUri=\"" + topicRoleTypeUri + "\", assocRoleTypeUri=\"" + assocRoleTypeUri + "\"";
238        logger.info(info);
239        try {
240            AssociationModelImpl assoc = fetchAssociationBetweenTopicAndAssociation(assocTypeUri, topicId, assocId,
241                topicRoleTypeUri, assocRoleTypeUri);
242            return assoc != null ? this.<Association>checkReadAccessAndInstantiate(assoc) : null;
243            // Note: inside a conditional operator the type witness is required (at least in Java 6)
244        } catch (Exception e) {
245            throw new RuntimeException("Fetching association failed (" + info + ")", e);
246        }
247    }
248
249    // ---
250
251    List<Association> getAssociationsByType(String assocTypeUri) {
252        try {
253            return checkReadAccessAndInstantiate(_getAssociationType(assocTypeUri).getAllInstances());
254        } catch (Exception e) {
255            throw new RuntimeException("Fetching associations by type failed (assocTypeUri=\"" + assocTypeUri + "\")",
256                e);
257        }
258    }
259
260    List<Association> getAssociations(long topic1Id, long topic2Id) {
261        return getAssociations(topic1Id, topic2Id, null);
262    }
263
264    List<Association> getAssociations(long topic1Id, long topic2Id, String assocTypeUri) {
265        logger.info("topic1Id=" + topic1Id + ", topic2Id=" + topic2Id + ", assocTypeUri=\"" + assocTypeUri + "\"");
266        try {
267            return checkReadAccessAndInstantiate(fetchAssociations(assocTypeUri, topic1Id, topic2Id, null, null));
268                                                                                 // roleTypeUri1=null, roleTypeUri2=null
269        } catch (Exception e) {
270            throw new RuntimeException("Fetching associations between topics " + topic1Id + " and " + topic2Id +
271                " failed (assocTypeUri=\"" + assocTypeUri + "\")", e);
272        }
273    }
274
275    // ---
276
277    Iterable<Association> getAllAssociations() {
278        return new AssociationIterable(this);
279    }
280
281    long[] getPlayerIds(long assocId) {
282        return fetchPlayerIds(assocId);
283    }
284
285    // ---
286
287    /**
288     * Convenience.
289     */
290    Association createAssociation(String typeUri, RoleModel roleModel1, RoleModel roleModel2) {
291        return createAssociation(mf.newAssociationModel(typeUri, roleModel1, roleModel2));
292    }
293
294    /**
295     * Creates a new association in the DB.
296     */
297    Association createAssociation(AssociationModelImpl model) {
298        try {
299            em.fireEvent(CoreEvent.PRE_CREATE_ASSOCIATION, model);
300            //
301            // 1) store in DB
302            storeAssociation(model);
303            valueStorage.storeValue(model);
304            createAssociationInstantiation(model.getId(), model.getTypeUri());
305            // 2) instantiate
306            Association assoc = new AssociationImpl(model, this);
307            //
308            em.fireEvent(CoreEvent.POST_CREATE_ASSOCIATION, assoc);
309            return assoc;
310        } catch (Exception e) {
311            throw new RuntimeException("Creating association failed (" + model + ")", e);
312        }
313    }
314
315    // ---
316
317    void updateAssociation(AssociationModel newModel) {
318        long assocId = newModel.getId();
319        try {
320            checkAssociationWriteAccess(assocId);
321            //
322            AssociationModelImpl model = fetchAssociation(assocId);
323            model.update(newModel);
324            //
325            // Note: there is no possible POST_UPDATE_ASSOCIATION_REQUEST event to fire here (compare to updateTopic()).
326            // It would be equivalent to POST_UPDATE_ASSOCIATION. Per request exactly one association is updated.
327            // Its childs are always topics (never associations).
328        } catch (Exception e) {
329            throw new RuntimeException("Updating association " + assocId + " failed", e);
330        }
331    }
332
333    void deleteAssociation(long assocId) {
334        try {
335            checkAssociationWriteAccess(assocId);
336            //
337            fetchAssociation(assocId).delete();
338        } catch (Exception e) {
339            throw new RuntimeException("Deleting association " + assocId + " failed", e);
340        }
341    }
342
343
344
345    // ===
346
347    void createTopicInstantiation(long topicId, String topicTypeUri) {
348        try {
349            AssociationModel assoc = mf.newAssociationModel("dm4.core.instantiation",
350                mf.newTopicRoleModel(topicTypeUri, "dm4.core.type"),
351                mf.newTopicRoleModel(topicId, "dm4.core.instance"));
352            storeAssociation(assoc);   // direct storage calls used here ### explain
353            storeAssociationValue(assoc.getId(), assoc.getSimpleValue());
354            createAssociationInstantiation(assoc.getId(), assoc.getTypeUri());
355        } catch (Exception e) {
356            throw new RuntimeException("Associating topic " + topicId +
357                " with topic type \"" + topicTypeUri + "\" failed", e);
358        }
359    }
360
361    void createAssociationInstantiation(long assocId, String assocTypeUri) {
362        try {
363            AssociationModel assoc = mf.newAssociationModel("dm4.core.instantiation",
364                mf.newTopicRoleModel(assocTypeUri, "dm4.core.type"),
365                mf.newAssociationRoleModel(assocId, "dm4.core.instance"));
366            storeAssociation(assoc);   // direct storage calls used here ### explain
367            storeAssociationValue(assoc.getId(), assoc.getSimpleValue());
368        } catch (Exception e) {
369            throw new RuntimeException("Associating association " + assocId +
370                " with association type \"" + assocTypeUri + "\" failed", e);
371        }
372    }
373
374
375
376    // === Types ===
377
378    TopicType getTopicType(String uri) {
379        TopicTypeModelImpl topicType = _getTopicType(uri);
380        if (!uri.equals("dm4.core.meta_meta_type")) {
381            checkReadAccess(topicType);
382        }
383        return topicType.instantiate();
384    }
385
386    TopicType getTopicTypeImplicitly(long topicId) {
387        checkTopicReadAccess(topicId);
388        return _getTopicType(typeUri(topicId)).instantiate();
389    }
390
391    // ---
392
393    AssociationType getAssociationType(String uri) {
394        return checkReadAccessAndInstantiate(_getAssociationType(uri));
395    }
396
397    AssociationType getAssociationTypeImplicitly(long assocId) {
398        checkAssociationReadAccess(assocId);
399        return _getAssociationType(typeUri(assocId)).instantiate();
400    }
401
402    // ---
403
404    List<TopicType> getAllTopicTypes() {
405        try {
406            List<TopicType> topicTypes = new ArrayList();
407            for (String uri : getTopicTypeUris()) {
408                topicTypes.add(_getTopicType(uri).instantiate());
409            }
410            return topicTypes;
411        } catch (Exception e) {
412            throw new RuntimeException("Fetching all topic types failed", e);
413        }
414    }
415
416    List<AssociationType> getAllAssociationTypes() {
417        try {
418            List<AssociationType> assocTypes = new ArrayList();
419            for (String uri : getAssociationTypeUris()) {
420                assocTypes.add(_getAssociationType(uri).instantiate());
421            }
422            return assocTypes;
423        } catch (Exception e) {
424            throw new RuntimeException("Fetching all association types failed", e);
425        }
426    }
427
428    // ---
429
430    TopicType createTopicType(TopicTypeModelImpl model) {
431        try {
432            // store in DB
433            createTopic(model, URI_PREFIX_TOPIC_TYPE);          // create generic topic
434            typeStorage.storeType(model);                       // store type-specific parts
435            //
436            TopicType topicType = model.instantiate();
437            em.fireEvent(CoreEvent.INTRODUCE_TOPIC_TYPE, topicType);
438            //
439            return topicType;
440        } catch (Exception e) {
441            throw new RuntimeException("Creating topic type \"" + model.getUri() + "\" failed (" + model + ")", e);
442        }
443    }
444
445    AssociationType createAssociationType(AssociationTypeModelImpl model) {
446        try {
447            // store in DB
448            createTopic(model, URI_PREFIX_ASSOCIATION_TYPE);    // create generic topic
449            typeStorage.storeType(model);                       // store type-specific parts
450            //
451            AssociationType assocType = model.instantiate();
452            em.fireEvent(CoreEvent.INTRODUCE_ASSOCIATION_TYPE, assocType);
453            //
454            return assocType;
455        } catch (Exception e) {
456            throw new RuntimeException("Creating association type \"" + model.getUri() + "\" failed (" + model + ")",
457                e);
458        }
459    }
460
461    // ---
462
463    Topic createRoleType(TopicModel model) {
464        // check type URI argument
465        String typeUri = model.getTypeUri();
466        if (typeUri == null) {
467            model.setTypeUri("dm4.core.role_type");
468        } else {
469            if (!typeUri.equals("dm4.core.role_type")) {
470                throw new IllegalArgumentException("A role type is supposed to be of type \"dm4.core.role_type\" " +
471                    "(found: \"" + typeUri + "\")");
472            }
473        }
474        //
475        return createTopic(model, URI_PREFIX_ROLE_TYPE);
476    }
477
478
479
480    // === Generic Object ===
481
482    DeepaMehtaObject getObject(long id) {
483        return checkReadAccessAndInstantiate(fetchObject(id));
484    }
485
486
487
488    // === Properties ===
489
490    List<Topic> getTopicsByProperty(String propUri, Object propValue) {
491        return checkReadAccessAndInstantiate(fetchTopicsByProperty(propUri, propValue));
492    }
493
494    List<Topic> getTopicsByPropertyRange(String propUri, Number from, Number to) {
495        return checkReadAccessAndInstantiate(fetchTopicsByPropertyRange(propUri, from, to));
496    }
497
498    List<Association> getAssociationsByProperty(String propUri, Object propValue) {
499        return checkReadAccessAndInstantiate(fetchAssociationsByProperty(propUri, propValue));
500    }
501
502    List<Association> getAssociationsByPropertyRange(String propUri, Number from, Number to) {
503        return checkReadAccessAndInstantiate(fetchAssociationsByPropertyRange(propUri, from, to));
504    }
505
506
507
508    // === Access Control / Instantiation ===
509
510    // These methods 1) instantiate objects from models, and 2) check the READ permission for each model.
511    // Call these methods when passing objects fetched from the DB to the user.
512    // ### TODO: make these private?
513
514    <O> O checkReadAccessAndInstantiate(DeepaMehtaObjectModelImpl model) {
515        checkReadAccess(model);
516        return (O) model.instantiate();
517    }
518
519    <O> List<O> checkReadAccessAndInstantiate(Iterable<? extends DeepaMehtaObjectModelImpl> models) {
520        return instantiate(filterReadables(models));
521    }
522
523    // ---
524
525    private <M extends DeepaMehtaObjectModelImpl> Iterable<M> filterReadables(Iterable<M> models) {
526        Iterator<? extends DeepaMehtaObjectModelImpl> i = models.iterator();
527        while (i.hasNext()) {
528            DeepaMehtaObjectModelImpl model = i.next();
529            try {
530                checkReadAccess(model);
531            } catch (AccessControlException e) {
532                i.remove();
533            }
534        }
535        return models;
536    }
537
538    /**
539     * @throws  AccessControlException
540     */
541    private void checkReadAccess(DeepaMehtaObjectModelImpl model) {
542        em.fireEvent(model.getReadAccessEvent(), model.getId());
543    }
544
545    // ---
546
547    private void checkTopicReadAccess(long topicId) {
548        em.fireEvent(CoreEvent.CHECK_TOPIC_READ_ACCESS, topicId);
549    }
550
551    private void checkAssociationReadAccess(long assocId) {
552        em.fireEvent(CoreEvent.CHECK_ASSOCIATION_READ_ACCESS, assocId);
553    }
554
555    // ---
556
557    private void checkTopicWriteAccess(long topicId) {
558        em.fireEvent(CoreEvent.CHECK_TOPIC_WRITE_ACCESS, topicId);
559    }
560
561    private void checkAssociationWriteAccess(long assocId) {
562        em.fireEvent(CoreEvent.CHECK_ASSOCIATION_WRITE_ACCESS, assocId);
563    }
564
565
566
567    // === Instantiation ===
568
569    <O> List<O> instantiate(Iterable<? extends DeepaMehtaObjectModelImpl> models) {
570        List<O> objects = new ArrayList();
571        for (DeepaMehtaObjectModelImpl model : models) {
572            objects.add((O) model.instantiate());
573        }
574        return objects;
575    }
576
577
578
579    // ===
580
581    private List<String> getTopicTypeUris() {
582        try {
583            List<String> topicTypeUris = new ArrayList();
584            // add meta types
585            topicTypeUris.add("dm4.core.topic_type");
586            topicTypeUris.add("dm4.core.assoc_type");
587            topicTypeUris.add("dm4.core.meta_type");
588            topicTypeUris.add("dm4.core.meta_meta_type");
589            // add regular types
590            for (TopicModel topicType : filterReadables(fetchTopics("type_uri", new SimpleValue(
591                                                                    "dm4.core.topic_type")))) {
592                topicTypeUris.add(topicType.getUri());
593            }
594            return topicTypeUris;
595        } catch (Exception e) {
596            throw new RuntimeException("Fetching list of topic type URIs failed", e);
597        }
598    }
599
600    private List<String> getAssociationTypeUris() {
601        try {
602            List<String> assocTypeUris = new ArrayList();
603            for (TopicModel assocType : filterReadables(fetchTopics("type_uri", new SimpleValue(
604                                                                    "dm4.core.assoc_type")))) {
605                assocTypeUris.add(assocType.getUri());
606            }
607            return assocTypeUris;
608        } catch (Exception e) {
609            throw new RuntimeException("Fetching list of association type URIs failed", e);
610        }
611    }
612
613    // ---
614
615    private TopicTypeModelImpl _getTopicType(String uri) {
616        return typeStorage.getTopicType(uri);
617    }
618
619    private AssociationTypeModelImpl _getAssociationType(String uri) {
620        return typeStorage.getAssociationType(uri);
621    }
622
623    // ---
624
625    private String typeUri(long objectId) {
626        return (String) fetchProperty(objectId, "type_uri");
627    }
628
629    // ---
630
631    private void bootstrapTypeCache() {
632        TopicTypeModelImpl metaMetaType = mf.newTopicTypeModel("dm4.core.meta_meta_type", "Meta Meta Type",
633            "dm4.core.text");
634        metaMetaType.setTypeUri("dm4.core.meta_meta_meta_type");
635        typeStorage.putInTypeCache(metaMetaType);
636    }
637}