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(typeStorage.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        try {
165            TopicModelImpl model = fetchTopic(newModel.getId());
166            model.update(newModel);
167            //
168            // Note: POST_UPDATE_TOPIC_REQUEST is fired only once per update request.
169            // On the other hand TopicModel's update() method is called multiple times while updating the child topics
170            // (see ChildTopicsModelImpl).
171            em.fireEvent(CoreEvent.POST_UPDATE_TOPIC_REQUEST, model.instantiate());
172        } catch (Exception e) {
173            throw new RuntimeException("Updating topic " + newModel.getId() + " failed", e);
174        }
175    }
176
177    void deleteTopic(long topicId) {
178        fetchTopic(topicId).delete();
179    }
180
181
182
183    // === Associations ===
184
185    Association getAssociation(long assocId) {
186        try {
187            return checkReadAccessAndInstantiate(fetchAssociation(assocId));
188        } catch (Exception e) {
189            throw new RuntimeException("Fetching association " + assocId + " failed", e);
190        }
191    }
192
193    Association getAssociationByValue(String key, SimpleValue value) {
194        try {
195            AssociationModelImpl assoc = fetchAssociation(key, value);
196            return assoc != null ? this.<Association>checkReadAccessAndInstantiate(assoc) : null;
197            // Note: inside a conditional operator the type witness is required (at least in Java 6)
198        } catch (Exception e) {
199            throw new RuntimeException("Fetching association failed (key=\"" + key + "\", value=\"" + value + "\")", e);
200        }
201    }
202
203    List<Association> getAssociationsByValue(String key, SimpleValue value) {
204        try {
205            return checkReadAccessAndInstantiate(fetchAssociations(key, value));
206        } catch (Exception e) {
207            throw new RuntimeException("Fetching associationss failed (key=\"" + key + "\", value=\"" + value + "\")",
208                e);
209        }
210    }
211
212    Association getAssociation(String assocTypeUri, long topic1Id, long topic2Id, String roleTypeUri1,
213                                                                                  String roleTypeUri2) {
214        String info = "assocTypeUri=\"" + assocTypeUri + "\", topic1Id=" + topic1Id + ", topic2Id=" + topic2Id +
215            ", roleTypeUri1=\"" + roleTypeUri1 + "\", roleTypeUri2=\"" + roleTypeUri2 + "\"";
216        try {
217            AssociationModelImpl assoc = fetchAssociation(assocTypeUri, topic1Id, topic2Id, roleTypeUri1, roleTypeUri2);
218            return assoc != null ? this.<Association>checkReadAccessAndInstantiate(assoc) : null;
219            // Note: inside a conditional operator the type witness is required (at least in Java 6)
220        } catch (Exception e) {
221            throw new RuntimeException("Fetching association failed (" + info + ")", e);
222        }
223    }
224
225    Association getAssociationBetweenTopicAndAssociation(String assocTypeUri, long topicId, long assocId,
226                                                         String topicRoleTypeUri, String assocRoleTypeUri) {
227        String info = "assocTypeUri=\"" + assocTypeUri + "\", topicId=" + topicId + ", assocId=" + assocId +
228            ", topicRoleTypeUri=\"" + topicRoleTypeUri + "\", assocRoleTypeUri=\"" + assocRoleTypeUri + "\"";
229        logger.info(info);
230        try {
231            AssociationModelImpl assoc = fetchAssociationBetweenTopicAndAssociation(assocTypeUri, topicId, assocId,
232                topicRoleTypeUri, assocRoleTypeUri);
233            return assoc != null ? this.<Association>checkReadAccessAndInstantiate(assoc) : null;
234            // Note: inside a conditional operator the type witness is required (at least in Java 6)
235        } catch (Exception e) {
236            throw new RuntimeException("Fetching association failed (" + info + ")", e);
237        }
238    }
239
240    // ---
241
242    List<Association> getAssociationsByType(String assocTypeUri) {
243        try {
244            return checkReadAccessAndInstantiate(typeStorage.getAssociationType(assocTypeUri).getAllInstances());
245        } catch (Exception e) {
246            throw new RuntimeException("Fetching associations by type failed (assocTypeUri=\"" + assocTypeUri + "\")",
247                e);
248        }
249    }
250
251    List<Association> getAssociations(long topic1Id, long topic2Id) {
252        return getAssociations(topic1Id, topic2Id, null);
253    }
254
255    List<Association> getAssociations(long topic1Id, long topic2Id, String assocTypeUri) {
256        logger.info("topic1Id=" + topic1Id + ", topic2Id=" + topic2Id + ", assocTypeUri=\"" + assocTypeUri + "\"");
257        try {
258            return checkReadAccessAndInstantiate(fetchAssociations(assocTypeUri, topic1Id, topic2Id, null, null));
259                                                                                 // roleTypeUri1=null, roleTypeUri2=null
260        } catch (Exception e) {
261            throw new RuntimeException("Fetching associations between topics " + topic1Id + " and " + topic2Id +
262                " failed (assocTypeUri=\"" + assocTypeUri + "\")", e);
263        }
264    }
265
266    // ---
267
268    Iterable<Association> getAllAssociations() {
269        return new AssociationIterable(this);
270    }
271
272    long[] getPlayerIds(long assocId) {
273        return fetchPlayerIds(assocId);
274    }
275
276    // ---
277
278    /**
279     * Convenience.
280     */
281    Association createAssociation(String typeUri, RoleModel roleModel1, RoleModel roleModel2) {
282        return createAssociation(mf.newAssociationModel(typeUri, roleModel1, roleModel2));
283    }
284
285    /**
286     * Creates a new association in the DB.
287     */
288    Association createAssociation(AssociationModelImpl model) {
289        try {
290            em.fireEvent(CoreEvent.PRE_CREATE_ASSOCIATION, model);
291            //
292            // 1) store in DB
293            storeAssociation(model);
294            valueStorage.storeValue(model);
295            createAssociationInstantiation(model.getId(), model.getTypeUri());
296            // 2) instantiate
297            Association assoc = new AssociationImpl(model, this);
298            //
299            em.fireEvent(CoreEvent.POST_CREATE_ASSOCIATION, assoc);
300            return assoc;
301        } catch (Exception e) {
302            throw new RuntimeException("Creating association failed (" + model + ")", e);
303        }
304    }
305
306    // ---
307
308    void updateAssociation(AssociationModel newModel) {
309        try {
310            AssociationModelImpl model = fetchAssociation(newModel.getId());
311            model.update(newModel);
312            //
313            // Note: there is no possible POST_UPDATE_ASSOCIATION_REQUEST event to fire here (compare to updateTopic()).
314            // It would be equivalent to POST_UPDATE_ASSOCIATION. Per request exactly one association is updated.
315            // Its childs are always topics (never associations).
316        } catch (Exception e) {
317            throw new RuntimeException("Updating association " + newModel.getId() + " failed", e);
318        }
319    }
320
321    void deleteAssociation(long assocId) {
322        fetchAssociation(assocId).delete();
323    }
324
325
326
327    // ===
328
329    void createTopicInstantiation(long topicId, String topicTypeUri) {
330        try {
331            AssociationModel assoc = mf.newAssociationModel("dm4.core.instantiation",
332                mf.newTopicRoleModel(topicTypeUri, "dm4.core.type"),
333                mf.newTopicRoleModel(topicId, "dm4.core.instance"));
334            storeAssociation(assoc);   // direct storage calls used here ### explain
335            storeAssociationValue(assoc.getId(), assoc.getSimpleValue());
336            createAssociationInstantiation(assoc.getId(), assoc.getTypeUri());
337        } catch (Exception e) {
338            throw new RuntimeException("Associating topic " + topicId +
339                " with topic type \"" + topicTypeUri + "\" failed", e);
340        }
341    }
342
343    void createAssociationInstantiation(long assocId, String assocTypeUri) {
344        try {
345            AssociationModel assoc = mf.newAssociationModel("dm4.core.instantiation",
346                mf.newTopicRoleModel(assocTypeUri, "dm4.core.type"),
347                mf.newAssociationRoleModel(assocId, "dm4.core.instance"));
348            storeAssociation(assoc);   // direct storage calls used here ### explain
349            storeAssociationValue(assoc.getId(), assoc.getSimpleValue());
350        } catch (Exception e) {
351            throw new RuntimeException("Associating association " + assocId +
352                " with association type \"" + assocTypeUri + "\" failed", e);
353        }
354    }
355
356
357
358    // === Types ===
359
360    TopicType getTopicType(String uri) {
361        return typeStorage.getTopicType(uri).instantiate();
362    }
363
364    AssociationType getAssociationType(String uri) {
365        return typeStorage.getAssociationType(uri).instantiate();
366    }
367
368    // ---
369
370    TopicType createTopicType(TopicTypeModelImpl model) {
371        try {
372            // store in DB
373            createTopic(model, URI_PREFIX_TOPIC_TYPE);          // create generic topic
374            typeStorage.storeType(model);                       // store type-specific parts
375            //
376            TopicType topicType = model.instantiate();
377            em.fireEvent(CoreEvent.INTRODUCE_TOPIC_TYPE, topicType);
378            //
379            return topicType;
380        } catch (Exception e) {
381            throw new RuntimeException("Creating topic type \"" + model.getUri() + "\" failed (" + model + ")", e);
382        }
383    }
384
385    AssociationType createAssociationType(AssociationTypeModelImpl model) {
386        try {
387            // store in DB
388            createTopic(model, URI_PREFIX_ASSOCIATION_TYPE);    // create generic topic
389            typeStorage.storeType(model);                       // store type-specific parts
390            //
391            AssociationType assocType = model.instantiate();
392            em.fireEvent(CoreEvent.INTRODUCE_ASSOCIATION_TYPE, assocType);
393            //
394            return assocType;
395        } catch (Exception e) {
396            throw new RuntimeException("Creating association type \"" + model.getUri() + "\" failed (" + model + ")",
397                e);
398        }
399    }
400
401    // ---
402
403    Topic createRoleType(TopicModel model) {
404        // check type URI argument
405        String typeUri = model.getTypeUri();
406        if (typeUri == null) {
407            model.setTypeUri("dm4.core.role_type");
408        } else {
409            if (!typeUri.equals("dm4.core.role_type")) {
410                throw new IllegalArgumentException("A role type is supposed to be of type \"dm4.core.role_type\" " +
411                    "(found: \"" + typeUri + "\")");
412            }
413        }
414        //
415        return createTopic(model, URI_PREFIX_ROLE_TYPE);
416    }
417
418
419
420    // === Generic Object ===
421
422    DeepaMehtaObject getObject(long id) {
423        DeepaMehtaObjectModelImpl model = fetchObject(id);
424        checkReadAccess(model);
425        return model.instantiate();
426    }
427
428
429
430    // === Properties ===
431
432    List<Topic> getTopicsByProperty(String propUri, Object propValue) {
433        return checkReadAccessAndInstantiate(fetchTopicsByProperty(propUri, propValue));
434    }
435
436    List<Topic> getTopicsByPropertyRange(String propUri, Number from, Number to) {
437        return checkReadAccessAndInstantiate(fetchTopicsByPropertyRange(propUri, from, to));
438    }
439
440    List<Association> getAssociationsByProperty(String propUri, Object propValue) {
441        return checkReadAccessAndInstantiate(fetchAssociationsByProperty(propUri, propValue));
442    }
443
444    List<Association> getAssociationsByPropertyRange(String propUri, Number from, Number to) {
445        return checkReadAccessAndInstantiate(fetchAssociationsByPropertyRange(propUri, from, to));
446    }
447
448
449
450    // === Access Control / Instantiation ===
451
452    // These methods 1) instantiate objects from models, and 2) check the READ permission for each model.
453    // Call these methods when passing objects fetched from the DB to the user.
454    // ### TODO: make these private?
455
456    <O> O checkReadAccessAndInstantiate(DeepaMehtaObjectModelImpl model) {
457        checkReadAccess(model);
458        return (O) model.instantiate();
459    }
460
461    <O> List<O> checkReadAccessAndInstantiate(Iterable<? extends DeepaMehtaObjectModelImpl> models) {
462        filterReadables(models);
463        return instantiate(models);
464    }
465
466    // ---
467
468    private void filterReadables(Iterable<? extends DeepaMehtaObjectModelImpl> models) {
469        Iterator<? extends DeepaMehtaObjectModelImpl> i = models.iterator();
470        while (i.hasNext()) {
471            DeepaMehtaObjectModelImpl model = i.next();
472            try {
473                checkReadAccess(model);
474            } catch (AccessControlException e) {
475                i.remove();
476            }
477        }
478    }
479
480    /**
481     * @throws  AccessControlException
482     */
483    private void checkReadAccess(DeepaMehtaObjectModelImpl model) {
484        em.fireEvent(model.getPreGetEvent(), model.getId());
485    }
486
487
488
489    // === Instantiation ===
490
491    <O> List<O> instantiate(Iterable<? extends DeepaMehtaObjectModelImpl> models) {
492        List<O> objects = new ArrayList();
493        for (DeepaMehtaObjectModelImpl model : models) {
494            objects.add((O) model.instantiate());
495        }
496        return objects;
497    }
498
499
500
501    // ===
502
503    private void bootstrapTypeCache() {
504        TopicTypeModelImpl metaMetaType = mf.newTopicTypeModel("dm4.core.meta_meta_type", "Meta Meta Type",
505            "dm4.core.text");
506        metaMetaType.setTypeUri("dm4.core.meta_meta_meta_type");
507        typeStorage.putInTypeCache(metaMetaType);
508    }
509}