001    package de.deepamehta.core.impl;
002    
003    import de.deepamehta.core.AssociationDefinition;
004    import de.deepamehta.core.DeepaMehtaObject;
005    import de.deepamehta.core.JSONEnabled;
006    import de.deepamehta.core.Type;
007    import de.deepamehta.core.ViewConfiguration;
008    import de.deepamehta.core.model.AssociationDefinitionModel;
009    import de.deepamehta.core.model.IndexMode;
010    import de.deepamehta.core.model.RoleModel;
011    import de.deepamehta.core.model.TypeModel;
012    import de.deepamehta.core.service.Directive;
013    import de.deepamehta.core.service.Directives;
014    import de.deepamehta.core.util.SequencedHashMap;
015    
016    import org.codehaus.jettison.json.JSONObject;
017    
018    import java.util.Collection;
019    import java.util.Iterator;
020    import java.util.List;
021    import java.util.logging.Logger;
022    
023    
024    
025    abstract class AttachedType extends AttachedTopic implements Type {
026    
027        // ---------------------------------------------------------------------------------------------- Instance Variables
028    
029        private SequencedHashMap<String, AssociationDefinition> assocDefs;  // Attached object cache
030        private ViewConfiguration viewConfig;                               // Attached object cache
031    
032        private Logger logger = Logger.getLogger(getClass().getName());
033    
034        // ---------------------------------------------------------------------------------------------------- Constructors
035    
036        AttachedType(TypeModel model, EmbeddedService dms) {
037            super(model, dms);
038            // init attached object cache
039            initAssocDefs();
040            initViewConfig();
041        }
042    
043        // -------------------------------------------------------------------------------------------------- Public Methods
044    
045    
046    
047        // ******************************************
048        // *** AttachedDeepaMehtaObject Overrides ***
049        // ******************************************
050    
051    
052    
053        // === Updating ===
054    
055        @Override
056        public void update(TypeModel model) {
057            boolean uriChanged = hasUriChanged(model.getUri());
058            if (uriChanged) {
059                _removeFromTypeCache();
060            }
061            //
062            super.update(model);
063            //
064            if (uriChanged) {
065                putInTypeCache();   // abstract
066            }
067            //
068            updateDataTypeUri(model.getDataTypeUri());
069            updateAssocDefs(model.getAssocDefs());
070            updateSequence(model.getAssocDefs());
071            updateLabelConfig(model.getLabelConfig());
072        }
073    
074    
075    
076        // === Deletion ===
077    
078        @Override
079        public void delete() {
080            String operation = "Deleting " + className() + " \"" + getUri() + "\" (named \"" + getSimpleValue() + "\")";
081            try {
082                logger.info(operation);
083                //
084                int size = getAllInstances().size();
085                if (size > 0) {
086                    throw new RuntimeException(size + " \"" + getSimpleValue() + "\" instances still exist");
087                }
088                //
089                super.delete();   // delete type topic
090                //
091                _removeFromTypeCache();
092            } catch (Exception e) {
093                throw new RuntimeException(operation + " failed", e);
094            }
095        }
096    
097    
098    
099        // ***************************
100        // *** Type Implementation ***
101        // ***************************
102    
103    
104    
105        // === Model ===
106    
107        // --- Data Type ---
108    
109        @Override
110        public String getDataTypeUri() {
111            return getModel().getDataTypeUri();
112        }
113    
114        @Override
115        public void setDataTypeUri(String dataTypeUri) {
116            // update memory
117            getModel().setDataTypeUri(dataTypeUri);
118            // update DB
119            storeDataTypeUri(dataTypeUri);
120        }
121    
122        // --- Index Modes ---
123    
124        @Override
125        public List<IndexMode> getIndexModes() {
126            return getModel().getIndexModes();
127        }
128    
129        @Override
130        public void addIndexMode(IndexMode indexMode) {
131            // update memory
132            getModel().addIndexMode(indexMode);
133            // update DB
134            dms.typeStorage.storeIndexMode(getUri(), indexMode);
135            indexAllInstances(indexMode);
136        }
137    
138        // --- Association Definitions ---
139    
140        @Override
141        public Collection<AssociationDefinition> getAssocDefs() {
142            return assocDefs.values();
143        }
144    
145        @Override
146        public AssociationDefinition getAssocDef(String childTypeUri) {
147            AssociationDefinition assocDef = assocDefs.get(childTypeUri);
148            if (assocDef == null) {
149                throw new RuntimeException("Schema violation: association definition \"" +
150                    childTypeUri + "\" not found in " + this);
151            }
152            return assocDef;
153        }
154    
155        @Override
156        public boolean hasAssocDef(String childTypeUri) {
157            return assocDefs.get(childTypeUri) != null;
158        }
159    
160        @Override
161        public Type addAssocDef(AssociationDefinitionModel assocDef) {
162            return addAssocDefBefore(assocDef, null);   // beforeChildTypeUri=null
163        }
164    
165        @Override
166        public Type addAssocDefBefore(AssociationDefinitionModel assocDef, String beforeChildTypeUri) {
167            // Note: the last assoc def must be determined *before* the memory is updated
168            AssociationDefinitionModel lastAssocDef = lastAssocDef();
169            // 1) update memory
170            getModel().addAssocDefBefore(assocDef, beforeChildTypeUri); // update model
171            _addAssocDefBefore(assocDef, beforeChildTypeUri);           // update attached object cache
172            // 2) update DB
173            dms.typeStorage.storeAssociationDefinition(assocDef);
174            // update sequence
175            long assocDefId = assocDef.getId();
176            if (beforeChildTypeUri == null) {
177                // append at end
178                dms.typeStorage.appendToSequence(getId(), assocDefId, lastAssocDef);
179            } else if (firstAssocDef().getId() == assocDefId) {
180                // insert at start
181                dms.typeStorage.insertAtSequenceStart(getId(), assocDefId);
182            } else {
183                // insert in the middle
184                long beforeAssocDefId = getAssocDef(beforeChildTypeUri).getId();
185                dms.typeStorage.insertIntoSequence(assocDefId, beforeAssocDefId);
186            }
187            return this;
188        }
189    
190        @Override
191        public void updateAssocDef(AssociationDefinitionModel assocDef) {
192            // update memory
193            getModel().updateAssocDef(assocDef);    // update model
194            _addAssocDef(assocDef);                 // update attached object cache
195            // update DB
196            // ### Note: the DB is not updated here! In case of interactive assoc type change the association is
197            // already updated in DB. => See interface comment.
198        }
199    
200        @Override
201        public Type removeAssocDef(String childTypeUri) {
202            // We trigger deleting an association definition by deleting the underlying association. This mimics deleting an
203            // association definition interactively in the webclient. Updating this type definition's memory and DB sequence
204            // is triggered then by the Type Editor plugin's preDeleteAssociation() hook.
205            // This way deleting an association definition works for both cases: 1) interactive deletion (when the user
206            // deletes an association), and 2) programmatical deletion (e.g. from a migration).
207            getAssocDef(childTypeUri).delete();
208            return this;
209        }
210    
211        // --- Label Configuration ---
212    
213        @Override
214        public List<String> getLabelConfig() {
215            return getModel().getLabelConfig();
216        }
217    
218        @Override
219        public void setLabelConfig(List<String> labelConfig) {
220            // update memory
221            getModel().setLabelConfig(labelConfig);
222            // update DB
223            dms.typeStorage.updateLabelConfig(labelConfig, getAssocDefs());
224        }
225    
226        // --- View Configuration ---
227    
228        @Override
229        public ViewConfiguration getViewConfig() {
230            return viewConfig;
231        }
232    
233        // FIXME: to be dropped
234        @Override
235        public Object getViewConfig(String typeUri, String settingUri) {
236            return getModel().getViewConfig(typeUri, settingUri);
237        }
238    
239        // ---
240    
241        @Override
242        public TypeModel getModel() {
243            return (TypeModel) super.getModel();
244        }
245    
246    
247    
248        // ----------------------------------------------------------------------------------------- Package Private Methods
249    
250        abstract void putInTypeCache();
251    
252        abstract void removeFromTypeCache();
253    
254        // ---
255    
256        abstract Directive getDeleteTypeDirective();
257    
258        abstract List<? extends DeepaMehtaObject> getAllInstances();
259    
260        // ---
261    
262        void removeAssocDefFromMemoryAndRebuildSequence(String childTypeUri) {
263            // update memory
264            getModel().removeAssocDef(childTypeUri);    // update model
265            _removeAssocDef(childTypeUri);              // update attached object cache
266            // update DB
267            dms.typeStorage.rebuildSequence(this);
268        }
269    
270        // ------------------------------------------------------------------------------------------------- Private Methods
271    
272    
273    
274        // === Update ===
275    
276        private boolean hasUriChanged(String newUri) {
277            return newUri != null && !getUri().equals(newUri);
278        }
279    
280        // ---
281    
282        private void updateDataTypeUri(String newDataTypeUri) {
283            if (newDataTypeUri != null) {
284                String dataTypeUri = getDataTypeUri();
285                if (!dataTypeUri.equals(newDataTypeUri)) {
286                    logger.info("### Changing data type URI from \"" + dataTypeUri + "\" -> \"" + newDataTypeUri + "\"");
287                    setDataTypeUri(newDataTypeUri);
288                }
289            }
290        }
291    
292        private void storeDataTypeUri(String dataTypeUri) {
293            // remove current assignment
294            getRelatedTopic("dm4.core.aggregation", "dm4.core.type", "dm4.core.default", "dm4.core.data_type")
295                .getRelatingAssociation().delete();
296            // create new assignment
297            dms.typeStorage.storeDataType(getUri(), dataTypeUri);
298        }
299    
300        // ---
301    
302        private void indexAllInstances(IndexMode indexMode) {
303            List<? extends DeepaMehtaObject> objects = getAllInstances();
304            //
305            String str = "\"" + getSimpleValue() + "\" (" + getUri() + ") instances";
306            if (getIndexModes().size() > 0) {
307                if (objects.size() > 0) {
308                    logger.info("### Indexing " + objects.size() + " " + str + " (indexMode=" + indexMode + ")");
309                } else {
310                    logger.info("### Indexing " + str + " ABORTED -- no instances in DB");
311                }
312            } else {
313                logger.info("### Indexing " + str + " ABORTED -- no index mode set");
314            }
315            //
316            for (DeepaMehtaObject obj : objects) {
317                dms.valueStorage.indexSimpleValue(obj.getModel(), indexMode);
318            }
319        }
320    
321        // ---
322    
323        private void updateAssocDefs(Collection<AssociationDefinitionModel> newAssocDefs) {
324            for (AssociationDefinitionModel assocDef : newAssocDefs) {
325                getAssocDef(assocDef.getChildTypeUri()).update(assocDef);
326            }
327        }
328    
329        // ---
330    
331        private void updateSequence(Collection<AssociationDefinitionModel> newAssocDefs) {
332            if (!hasSequenceChanged(newAssocDefs)) {
333                return;
334            }
335            logger.info("### Changing assoc def sequence");
336            // update memory
337            getModel().removeAllAssocDefs();
338            for (AssociationDefinitionModel assocDef : newAssocDefs) {
339                getModel().addAssocDef(assocDef);
340            }
341            initAssocDefs();    // attached object cache
342            // update DB
343            dms.typeStorage.rebuildSequence(this);
344        }
345    
346        private boolean hasSequenceChanged(Collection<AssociationDefinitionModel> newAssocDefs) {
347            Collection<AssociationDefinition> assocDefs = getAssocDefs();
348            if (assocDefs.size() != newAssocDefs.size()) {
349                throw new RuntimeException("adding/removing of assoc defs not yet supported via updateTopicType() call");
350            }
351            //
352            Iterator<AssociationDefinitionModel> i = newAssocDefs.iterator();
353            for (AssociationDefinition assocDef : assocDefs) {
354                AssociationDefinitionModel newAssocDef = i.next();
355                if (!assocDef.getChildTypeUri().equals(newAssocDef.getChildTypeUri())) {
356                    return true;
357                }
358            }
359            //
360            return false;
361        }
362    
363        // ---
364    
365        private void updateLabelConfig(List<String> newLabelConfig) {
366            if (!getLabelConfig().equals(newLabelConfig)) {
367                logger.info("### Changing label configuration");
368                setLabelConfig(newLabelConfig);
369            }
370        }
371    
372    
373    
374        // === Helper ===
375    
376        /**
377         * Returns the last association definition of this type or
378         * <code>null</code> if there are no association definitions.
379         *
380         * ### TODO: move to class TypeModel?
381         */
382        private AssociationDefinitionModel lastAssocDef() {
383            AssociationDefinitionModel lastAssocDef = null;
384            for (AssociationDefinitionModel assocDef : getModel().getAssocDefs()) {
385                lastAssocDef = assocDef;
386            }
387            return lastAssocDef;
388        }
389    
390        private AssociationDefinitionModel firstAssocDef() {
391            return getModel().getAssocDefs().iterator().next();
392        }
393    
394        // --- Attached Object Cache ---
395    
396        // ### FIXME: make it private
397        protected void initAssocDefs() {
398            this.assocDefs = new SequencedHashMap();
399            for (AssociationDefinitionModel model : getModel().getAssocDefs()) {
400                _addAssocDef(model);
401            }
402        }
403    
404        private void _addAssocDef(AssociationDefinitionModel model) {
405            _addAssocDefBefore(model, null);    // beforeChildTypeUri=null
406        }
407    
408        /**
409         * @param   beforeChildTypeUri  the assoc def <i>before</i> the assoc def is inserted into the sequence.
410         *                              If <code>null</code> the assoc def is appended at the end.
411         */
412        private void _addAssocDefBefore(AssociationDefinitionModel model, String beforeChildTypeUri) {
413            assocDefs.putBefore(model.getChildTypeUri(), new AttachedAssociationDefinition(model, dms), beforeChildTypeUri);
414        }
415    
416        private void _removeAssocDef(String childTypeUri) {
417            // error check
418            getAssocDef(childTypeUri);
419            //
420            assocDefs.remove(childTypeUri);
421        }
422    
423        // ---
424    
425        private void initViewConfig() {
426            RoleModel configurable = dms.typeStorage.createConfigurableType(getId());   // ### type ID is uninitialized
427            this.viewConfig = new AttachedViewConfiguration(configurable, getModel().getViewConfigModel(), dms);
428        }
429    
430    
431    
432        // ===
433    
434        /**
435         * Removes this type from type cache and adds a DELETE TYPE directive to the given set of directives.
436         */
437        private void _removeFromTypeCache() {
438            removeFromTypeCache();                      // abstract
439            addDeleteTypeDirective();
440        }
441    
442        private void addDeleteTypeDirective() {
443            Directive dir = getDeleteTypeDirective();   // abstract
444            Directives.get().add(dir, new JSONWrapper("uri", getUri()));
445        }
446    
447        // ------------------------------------------------------------------------------------------------- Private Classes
448    
449        private class JSONWrapper implements JSONEnabled {
450    
451            private JSONObject wrapped;
452    
453            private JSONWrapper(String key, Object value) {
454                try {
455                    wrapped = new JSONObject();
456                    wrapped.put(key, value);
457                } catch (Exception e) {
458                    throw new RuntimeException("Constructing a JSONWrapper failed", e);
459                }
460            }
461    
462            @Override
463            public JSONObject toJSON() {
464                return wrapped;
465            }
466        }
467    }