001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (C) Alkacon Software (http://www.alkacon.com)
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * For further information about Alkacon Software, please see the
018 * company website: http://www.alkacon.com
019 *
020 * For further information about OpenCms, please see the
021 * project website: http://www.opencms.org
022 *
023 * You should have received a copy of the GNU Lesser General Public
024 * License along with this library; if not, write to the Free Software
025 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
026 */
027
028package org.opencms.acacia.shared;
029
030import org.opencms.acacia.shared.CmsEntityChangeEvent.ChangeType;
031import org.opencms.gwt.shared.CmsGwtLog;
032
033import java.io.Serializable;
034import java.util.ArrayList;
035import java.util.HashMap;
036import java.util.List;
037import java.util.Map;
038import java.util.Map.Entry;
039
040import com.google.common.collect.Lists;
041import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
042import com.google.gwt.event.logical.shared.ValueChangeEvent;
043import com.google.gwt.event.logical.shared.ValueChangeHandler;
044import com.google.gwt.event.shared.EventHandler;
045import com.google.gwt.event.shared.GwtEvent;
046import com.google.gwt.event.shared.HandlerRegistration;
047import com.google.gwt.event.shared.SimpleEventBus;
048
049/**
050 * Serializable entity implementation.<p>
051 */
052public class CmsEntity implements HasValueChangeHandlers<CmsEntity>, Serializable {
053
054    /**
055     * Handles child entity changes.<p>
056     */
057    protected class EntityChangeHandler implements ValueChangeHandler<CmsEntity> {
058
059        /**
060         * @see com.google.gwt.event.logical.shared.ValueChangeHandler#onValueChange(com.google.gwt.event.logical.shared.ValueChangeEvent)
061         */
062        public void onValueChange(ValueChangeEvent<CmsEntity> event) {
063
064            ChangeType type = ((CmsEntityChangeEvent)event).getChangeType();
065            fireChange(type);
066        }
067    }
068
069    /** The serial version id. */
070    private static final long serialVersionUID = -6933931178070025267L;
071
072    /** The entity attribute values. */
073    private Map<String, List<CmsEntity>> m_entityAttributes;
074
075    /** The entity id. */
076    private String m_id;
077
078    /** The simple attribute values. */
079    private Map<String, List<String>> m_simpleAttributes;
080
081    /** The type name. */
082    private String m_typeName;
083
084    /** The event bus. */
085    private transient SimpleEventBus m_eventBus;
086
087    /** The child entites change handler. */
088    private transient EntityChangeHandler m_childChangeHandler = new EntityChangeHandler();
089
090    /** The handler registrations. */
091    private transient Map<String, HandlerRegistration> m_changeHandlerRegistry;
092
093    /**
094     * Constructor.<p>
095     *
096     * @param id the entity id/URI
097     * @param typeName the entity type name
098     */
099    public CmsEntity(String id, String typeName) {
100
101        this();
102        m_id = id;
103        m_typeName = typeName;
104    }
105
106    /**
107     * Constructor. For serialization only.<p>
108     */
109    protected CmsEntity() {
110
111        m_simpleAttributes = new HashMap<String, List<String>>();
112        m_entityAttributes = new HashMap<String, List<CmsEntity>>();
113        m_changeHandlerRegistry = new HashMap<String, HandlerRegistration>();
114    }
115
116    /**
117     * Returns the value of a simple attribute for the given path or <code>null</code>, if the value does not exist.<p>
118     *
119     * @param entity the entity to get the value from
120     * @param pathElements the path elements
121     *
122     * @return the value
123     */
124    public static String getValueForPath(CmsEntity entity, String[] pathElements) {
125
126        String result = null;
127        if ((pathElements != null) && (pathElements.length >= 1)) {
128            String attributeName = pathElements[0];
129            int index = CmsContentDefinition.extractIndex(attributeName);
130            if (index > 0) {
131                index--;
132            }
133            attributeName = entity.getTypeName() + "/" + CmsContentDefinition.removeIndex(attributeName);
134            CmsEntityAttribute attribute = entity.getAttribute(attributeName);
135            if (!((attribute == null) || (attribute.isComplexValue() && (pathElements.length == 1)))) {
136                if (attribute.isSimpleValue()) {
137                    if ((pathElements.length == 1) && (attribute.getValueCount() > 0)) {
138                        List<String> values = attribute.getSimpleValues();
139                        result = values.get(index);
140                    }
141                } else if (attribute.getValueCount() > (index)) {
142                    String[] childPathElements = new String[pathElements.length - 1];
143                    for (int i = 1; i < pathElements.length; i++) {
144                        childPathElements[i - 1] = pathElements[i];
145                    }
146                    List<CmsEntity> values = attribute.getComplexValues();
147                    result = getValueForPath(values.get(index), childPathElements);
148                }
149            }
150        }
151        return result;
152    }
153
154    /**
155     * Gets the list of values reachable from the given base object with the given path.<p>
156     *
157     * @param baseObject the base object (a CmsEntity or a string)
158     * @param pathComponents the path components
159     * @return the list of values for the given path (either of type String or CmsEntity)
160     */
161    public static List<Object> getValuesForPath(Object baseObject, String[] pathComponents) {
162
163        List<Object> currentList = Lists.newArrayList();
164        currentList.add(baseObject);
165        for (String pathComponent : pathComponents) {
166            List<Object> newList = Lists.newArrayList();
167            for (Object element : currentList) {
168                newList.addAll(getValuesForPathComponent(element, pathComponent));
169            }
170            currentList = newList;
171        }
172        return currentList;
173    }
174
175    /**
176     * Gets the values reachable from a given object (an entity or a string) with a single XPath component.<p>
177     *
178     * If entityOrString is a string, and pathComponent is "VALUE", a list containing only entityOrString is returned.
179     * Otherwise, entityOrString is assumed to be an entity, and the pathComponent is interpreted as a field of the entity
180     * (possibly with an index).
181     *
182     * @param entityOrString the entity or string from which to get the values for the given path component
183     * @param pathComponent the path component
184     * @return the list of reachable values
185     */
186    public static List<Object> getValuesForPathComponent(Object entityOrString, String pathComponent) {
187
188        List<Object> result = Lists.newArrayList();
189        if (pathComponent.equals("VALUE")) {
190            result.add(entityOrString);
191        } else {
192            if (entityOrString instanceof CmsEntity) {
193                CmsEntity entity = (CmsEntity)entityOrString;
194                boolean hasIndex = CmsContentDefinition.hasIndex(pathComponent);
195                int index = CmsContentDefinition.extractIndex(pathComponent);
196                if (index > 0) {
197                    index--;
198                }
199                String attributeName = entity.getTypeName() + "/" + CmsContentDefinition.removeIndex(pathComponent);
200                CmsEntityAttribute attribute = entity.getAttribute(attributeName);
201
202                if (attribute != null) {
203                    if (hasIndex) {
204                        if (index < attribute.getValueCount()) {
205                            if (attribute.isSimpleValue()) {
206                                result.add(attribute.getSimpleValues().get(index));
207                            } else {
208                                result.add(attribute.getComplexValues().get(index));
209                            }
210                        }
211                    } else {
212                        if (attribute.isSimpleValue()) {
213                            result.addAll(attribute.getSimpleValues());
214                        } else {
215                            result.addAll(attribute.getComplexValues());
216                        }
217                    }
218                }
219            }
220        }
221        return result;
222    }
223
224    /**
225     * Adds the given attribute value.<p>
226     *
227     * @param attributeName the attribute name
228     * @param value the attribute value
229     */
230    public void addAttributeValue(String attributeName, CmsEntity value) {
231
232        if (m_simpleAttributes.containsKey(attributeName)) {
233            throw new RuntimeException("Attribute already exists with a simple type value.");
234        }
235        if (m_entityAttributes.containsKey(attributeName)) {
236            m_entityAttributes.get(attributeName).add(value);
237        } else {
238            List<CmsEntity> values = new ArrayList<CmsEntity>();
239            values.add(value);
240            m_entityAttributes.put(attributeName, values);
241        }
242        registerChangeHandler(value);
243        fireChange(ChangeType.add);
244    }
245
246    /**
247     * Adds the given attribute value.<p>
248     *
249     * @param attributeName the attribute name
250     * @param value the attribute value
251     */
252    public void addAttributeValue(String attributeName, String value) {
253
254        if (m_entityAttributes.containsKey(attributeName)) {
255            throw new RuntimeException("Attribute already exists with a entity type value.");
256        }
257        if (m_simpleAttributes.containsKey(attributeName)) {
258            m_simpleAttributes.get(attributeName).add(value);
259        } else {
260            List<String> values = new ArrayList<String>();
261            values.add(value);
262            m_simpleAttributes.put(attributeName, values);
263        }
264        fireChange(ChangeType.add);
265    }
266
267    /**
268     * @see com.google.gwt.event.logical.shared.HasValueChangeHandlers#addValueChangeHandler(com.google.gwt.event.logical.shared.ValueChangeHandler)
269     */
270    public HandlerRegistration addValueChangeHandler(ValueChangeHandler<CmsEntity> handler) {
271
272        return addHandler(handler, ValueChangeEvent.getType());
273    }
274
275    /**
276     * Clones the given entity keeping all entity ids.<p>
277     *
278     * @return returns the cloned instance
279     */
280    public CmsEntity cloneEntity() {
281
282        CmsEntity clone = new CmsEntity(getId(), getTypeName());
283        for (CmsEntityAttribute attribute : getAttributes()) {
284            if (attribute.isSimpleValue()) {
285                List<String> values = attribute.getSimpleValues();
286                for (String value : values) {
287                    clone.addAttributeValue(attribute.getAttributeName(), value);
288                }
289            } else {
290                List<CmsEntity> values = attribute.getComplexValues();
291                for (CmsEntity value : values) {
292                    clone.addAttributeValue(attribute.getAttributeName(), value.cloneEntity());
293                }
294            }
295        }
296        return clone;
297    }
298
299    /**
300     * Creates a deep copy of this entity.<p>
301     *
302     * @param entityId the id of the new entity, if <code>null</code> a generic id will be used
303     *
304     * @return the entity copy
305     */
306    public CmsEntity createDeepCopy(String entityId) {
307
308        CmsEntity result = new CmsEntity(entityId, getTypeName());
309        for (CmsEntityAttribute attribute : getAttributes()) {
310            if (attribute.isSimpleValue()) {
311                List<String> values = attribute.getSimpleValues();
312                for (String value : values) {
313                    result.addAttributeValue(attribute.getAttributeName(), value);
314                }
315            } else {
316                List<CmsEntity> values = attribute.getComplexValues();
317                for (CmsEntity value : values) {
318                    result.addAttributeValue(attribute.getAttributeName(), value.createDeepCopy(null));
319                }
320            }
321        }
322        return result;
323    }
324
325    /**
326     * Ensures that the change event is also fired on child entity change.<p>
327     */
328    public void ensureChangeHandlers() {
329
330        if (!m_changeHandlerRegistry.isEmpty()) {
331            for (HandlerRegistration reg : m_changeHandlerRegistry.values()) {
332                reg.removeHandler();
333            }
334            m_changeHandlerRegistry.clear();
335        }
336        for (List<CmsEntity> attr : m_entityAttributes.values()) {
337            for (CmsEntity child : attr) {
338                registerChangeHandler(child);
339                child.ensureChangeHandlers();
340            }
341        }
342    }
343
344    /**
345     * @see java.lang.Object#equals(java.lang.Object)
346     */
347    @Override
348    public boolean equals(Object obj) {
349
350        boolean result = false;
351        if (obj instanceof CmsEntity) {
352            CmsEntity test = (CmsEntity)obj;
353            if (m_simpleAttributes.keySet().equals(test.m_simpleAttributes.keySet())
354                && m_entityAttributes.keySet().equals(test.m_entityAttributes.keySet())) {
355                result = true;
356                for (String attributeName : m_simpleAttributes.keySet()) {
357                    if (!m_simpleAttributes.get(attributeName).equals(test.m_simpleAttributes.get(attributeName))) {
358                        result = false;
359                        break;
360                    }
361                }
362                if (result) {
363                    for (String attributeName : m_entityAttributes.keySet()) {
364                        if (!m_entityAttributes.get(attributeName).equals(test.m_entityAttributes.get(attributeName))) {
365                            result = false;
366                            break;
367                        }
368                    }
369                }
370            }
371        }
372        return result;
373    }
374
375    /**
376     * @see com.google.gwt.event.shared.HasHandlers#fireEvent(com.google.gwt.event.shared.GwtEvent)
377     */
378    public void fireEvent(GwtEvent<?> event) {
379
380        ensureHandlers().fireEventFromSource(event, this);
381    }
382
383    /**
384     * Returns an attribute.<p>
385     *
386     * @param attributeName the attribute name
387     *
388     * @return the attribute value
389     */
390    public CmsEntityAttribute getAttribute(String attributeName) {
391
392        if (m_simpleAttributes.containsKey(attributeName)) {
393            return CmsEntityAttribute.createSimpleAttribute(attributeName, m_simpleAttributes.get(attributeName));
394        }
395        if (m_entityAttributes.containsKey(attributeName)) {
396            return CmsEntityAttribute.createEntityAttribute(attributeName, m_entityAttributes.get(attributeName));
397        }
398        return null;
399    }
400
401    /**
402     * Returns all entity attributes.<p>
403     *
404     * @return the entity attributes
405     */
406    public List<CmsEntityAttribute> getAttributes() {
407
408        List<CmsEntityAttribute> result = new ArrayList<CmsEntityAttribute>();
409        for (String name : m_simpleAttributes.keySet()) {
410            result.add(getAttribute(name));
411        }
412        for (String name : m_entityAttributes.keySet()) {
413            result.add(getAttribute(name));
414        }
415        return result;
416    }
417
418    /**
419     * Returns this or a child entity with the given id.<p>
420     * Will return <code>null</code> if no entity with the given id is present.<p>
421     *
422     * @param entityId the entity id
423     *
424     * @return the entity
425     */
426    public CmsEntity getEntityById(String entityId) {
427
428        CmsEntity result = null;
429        if (m_id.equals(entityId)) {
430            result = this;
431        } else {
432            for (List<CmsEntity> children : m_entityAttributes.values()) {
433                for (CmsEntity child : children) {
434                    result = child.getEntityById(entityId);
435                    if (result != null) {
436                        break;
437                    }
438                }
439                if (result != null) {
440                    break;
441                }
442            }
443        }
444        return result;
445    }
446
447    /**
448     * Returns the entity id.<p>
449     *
450     * @return the id
451     */
452    public String getId() {
453
454        return m_id;
455    }
456
457    /**
458     * Returns the entity type name.<p>
459     *
460     * @return the entity type name
461     */
462    public String getTypeName() {
463
464        return m_typeName;
465    }
466
467    /**
468     * Returns if the entity has the given attribute.<p>
469     *
470     * @param attributeName the attribute name
471     *
472     * @return <code>true</code> if the entity has the given attribute
473     */
474    public boolean hasAttribute(String attributeName) {
475
476        return m_simpleAttributes.containsKey(attributeName) || m_entityAttributes.containsKey(attributeName);
477    }
478
479    /**
480     * @see java.lang.Object#hashCode()
481     */
482    @Override
483    public int hashCode() {
484
485        return super.hashCode();
486    }
487
488    /**
489     * Inserts a new attribute value at the given index.<p>
490     *
491     * @param attributeName the attribute name
492     * @param value the attribute value
493     * @param index the value index
494     */
495    public void insertAttributeValue(String attributeName, CmsEntity value, int index) {
496
497        if (m_entityAttributes.containsKey(attributeName)) {
498            m_entityAttributes.get(attributeName).add(index, value);
499        } else {
500            setAttributeValue(attributeName, value);
501        }
502        registerChangeHandler(value);
503        fireChange(ChangeType.add);
504    }
505
506    /**
507     * Inserts a new attribute value at the given index.<p>
508     *
509     * @param attributeName the attribute name
510     * @param value the attribute value
511     * @param index the value index
512     */
513    public void insertAttributeValue(String attributeName, String value, int index) {
514
515        if (m_simpleAttributes.containsKey(attributeName)) {
516            m_simpleAttributes.get(attributeName).add(index, value);
517        } else {
518            setAttributeValue(attributeName, value);
519        }
520        fireChange(ChangeType.add);
521    }
522
523    /**
524     * Removes the given attribute.<p>
525     *
526     * @param attributeName the attribute name
527     */
528    public void removeAttribute(String attributeName) {
529
530        removeAttributeSilent(attributeName);
531        fireChange(ChangeType.remove);
532    }
533
534    /**
535     * Removes the attribute without triggering any change events.<p>
536     *
537     * @param attributeName the attribute name
538     */
539    public void removeAttributeSilent(String attributeName) {
540
541        CmsEntityAttribute attr = getAttribute(attributeName);
542        if (attr != null) {
543            if (attr.isSimpleValue()) {
544                m_simpleAttributes.remove(attributeName);
545            } else {
546                for (CmsEntity child : attr.getComplexValues()) {
547                    removeChildChangeHandler(child);
548                }
549                m_entityAttributes.remove(attributeName);
550            }
551        }
552    }
553
554    /**
555     * Removes a specific attribute value.<p>
556     *
557     * @param attributeName the attribute name
558     * @param index the value index
559     */
560    public void removeAttributeValue(String attributeName, int index) {
561
562        if (m_simpleAttributes.containsKey(attributeName)) {
563            List<String> values = m_simpleAttributes.get(attributeName);
564            if ((values.size() == 1) && (index == 0)) {
565                removeAttributeSilent(attributeName);
566            } else {
567                values.remove(index);
568            }
569        } else if (m_entityAttributes.containsKey(attributeName)) {
570            List<CmsEntity> values = m_entityAttributes.get(attributeName);
571            if ((values.size() == 1) && (index == 0)) {
572                removeAttributeSilent(attributeName);
573            } else {
574                CmsEntity child = values.remove(index);
575                removeChildChangeHandler(child);
576            }
577        }
578        fireChange(ChangeType.remove);
579    }
580
581    /**
582     * Sets the given attribute value. Will remove all previous attribute values.<p>
583     *
584     * @param attributeName the attribute name
585     * @param value the attribute value
586     */
587    public void setAttributeValue(String attributeName, CmsEntity value) {
588
589        // make sure there is no attribute value set
590        removeAttributeSilent(attributeName);
591        addAttributeValue(attributeName, value);
592    }
593
594    /**
595     * Sets the given attribute value at the given index.<p>
596     *
597     * @param attributeName the attribute name
598     * @param value the attribute value
599     * @param index the value index
600     */
601    public void setAttributeValue(String attributeName, CmsEntity value, int index) {
602
603        if (m_simpleAttributes.containsKey(attributeName)) {
604            throw new RuntimeException("Attribute already exists with a simple type value.");
605        }
606        if (!m_entityAttributes.containsKey(attributeName)) {
607            if (index != 0) {
608                throw new IndexOutOfBoundsException();
609            } else {
610                addAttributeValue(attributeName, value);
611            }
612        } else {
613            if (m_entityAttributes.get(attributeName).size() > index) {
614                CmsEntity child = m_entityAttributes.get(attributeName).remove(index);
615                removeChildChangeHandler(child);
616            }
617            m_entityAttributes.get(attributeName).add(index, value);
618            fireChange(ChangeType.change);
619        }
620    }
621
622    /**
623     * Sets the given attribute value. Will remove all previous attribute values.<p>
624     *
625     * @param attributeName the attribute name
626     * @param value the attribute value
627     */
628    public void setAttributeValue(String attributeName, String value) {
629
630        m_entityAttributes.remove(attributeName);
631        List<String> values = new ArrayList<String>();
632        values.add(value);
633        m_simpleAttributes.put(attributeName, values);
634        fireChange(ChangeType.change);
635    }
636
637    /**
638     * Sets the given attribute value at the given index.<p>
639     *
640     * @param attributeName the attribute name
641     * @param value the attribute value
642     * @param index the value index
643     */
644    public void setAttributeValue(String attributeName, String value, int index) {
645
646        if (m_entityAttributes.containsKey(attributeName)) {
647            throw new RuntimeException("Attribute already exists with a simple type value.");
648        }
649        if (!m_simpleAttributes.containsKey(attributeName)) {
650            if (index != 0) {
651                throw new IndexOutOfBoundsException();
652            } else {
653                addAttributeValue(attributeName, value);
654            }
655        } else {
656            if (m_simpleAttributes.get(attributeName).size() > index) {
657                m_simpleAttributes.get(attributeName).remove(index);
658            }
659            m_simpleAttributes.get(attributeName).add(index, value);
660            fireChange(ChangeType.change);
661        }
662    }
663
664    /**
665     * Returns the JSON string representation of this entity.<p>
666     *
667     * @return the JSON string representation of this entity
668     */
669    public String toJSON() {
670
671        StringBuffer result = new StringBuffer();
672        result.append("{\n");
673        for (Entry<String, List<String>> simpleEntry : m_simpleAttributes.entrySet()) {
674            result.append("\"").append(simpleEntry.getKey()).append("\"").append(": [\n");
675            boolean firstValue = true;
676            for (String value : simpleEntry.getValue()) {
677                if (firstValue) {
678                    firstValue = false;
679                } else {
680                    result.append(",\n");
681                }
682                result.append("\"").append(value).append("\"");
683            }
684            result.append("],\n");
685        }
686        for (Entry<String, List<CmsEntity>> entityEntry : m_entityAttributes.entrySet()) {
687            result.append("\"").append(entityEntry.getKey()).append("\"").append(": [\n");
688            boolean firstValue = true;
689            for (CmsEntity value : entityEntry.getValue()) {
690                if (firstValue) {
691                    firstValue = false;
692                } else {
693                    result.append(",\n");
694                }
695                result.append(value.toJSON());
696            }
697            result.append("],\n");
698        }
699        result.append("\"id\": \"").append(m_id).append("\"");
700        result.append("}");
701        return result.toString();
702    }
703
704    /**
705     * @see java.lang.Object#toString()
706     */
707    @Override
708    public String toString() {
709
710        return toJSON();
711    }
712
713    /**
714     * Adds this handler to the widget.
715     *
716     * @param <H> the type of handler to add
717     * @param type the event type
718     * @param handler the handler
719     * @return {@link HandlerRegistration} used to remove the handler
720     */
721    protected final <H extends EventHandler> HandlerRegistration addHandler(final H handler, GwtEvent.Type<H> type) {
722
723        return ensureHandlers().addHandlerToSource(type, this, handler);
724    }
725
726    /**
727     * Fires the change event for this entity.<p>
728     *
729     * @param type the change type
730     */
731    void fireChange(ChangeType type) {
732
733        CmsEntityChangeEvent event = new CmsEntityChangeEvent(this, type);
734        fireEvent(event);
735    }
736
737    /**
738     * Lazy initializing the handler manager.<p>
739     *
740     * @return the handler manager
741     */
742    private SimpleEventBus ensureHandlers() {
743
744        if (m_eventBus == null) {
745            m_eventBus = new SimpleEventBus();
746        }
747        return m_eventBus;
748    }
749
750    /**
751     * Adds the value change handler to the given entity.<p>
752     *
753     * @param child the child entity
754     */
755    private void registerChangeHandler(CmsEntity child) {
756
757        HandlerRegistration reg = child.addValueChangeHandler(m_childChangeHandler);
758        m_changeHandlerRegistry.put(child.getId(), reg);
759    }
760
761    /**
762     * Removes the child entity change handler.<p>
763     *
764     * @param child the child entity
765     */
766    private void removeChildChangeHandler(CmsEntity child) {
767
768        HandlerRegistration reg = m_changeHandlerRegistry.remove(child.getId());
769        if (reg != null) {
770            reg.removeHandler();
771        }
772    }
773}