001/*
002 * This library is part of OpenCms -
003 * the Open Source Content Management System
004 *
005 * Copyright (c) Alkacon Software GmbH & Co. KG (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 GmbH & Co. KG, 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.file;
029
030import org.opencms.i18n.CmsLocaleManager;
031import org.opencms.main.CmsRuntimeException;
032import org.opencms.util.CmsStringUtil;
033
034import java.io.Serializable;
035import java.util.ArrayList;
036import java.util.Collections;
037import java.util.HashMap;
038import java.util.Iterator;
039import java.util.LinkedHashMap;
040import java.util.List;
041import java.util.Locale;
042import java.util.Map;
043import java.util.RandomAccess;
044
045import org.apache.commons.collections.Transformer;
046
047/**
048 * Represents a property (meta-information) mapped to a VFS resource.<p>
049 *
050 * A property is an object that contains three string values: a name, a property value which is mapped
051 * to the structure record of a resource, and a property value which is mapped to the resource
052 * record of a resource. A property object is valid if it has both values or just one value set.
053 * Each property needs at least a name and one value set.<p>
054 *
055 * A property value mapped to the structure record of a resource is significant for a single
056 * resource (sibling). A property value mapped to the resource record of a resource is significant
057 * for all siblings of a resource record. This is possible by getting the "compound value"
058 * (see {@link #getValue()}) of a property in case a property object has both values set. The compound
059 * value of a property object is the value mapped to the structure record, because it's structure
060 * value is more significant than it's resource value. This allows to set a property only one time
061 * on the resource record, and the property takes effect on all siblings of this resource record.<p>
062 *
063 * The ID of the structure or resource record where a property value is mapped to is represented by
064 * the "PROPERTY_MAPPING_ID" table attribute in the database. The "PROPERTY_MAPPING_TYPE" table
065 * attribute (see {@link #STRUCTURE_RECORD_MAPPING} and {@link #RESOURCE_RECORD_MAPPING})
066 * determines whether the value of the "PROPERTY_MAPPING_ID" attribute of the current row is
067 * a structure or resource record ID.<p>
068 *
069 * Property objects are written to the database using {@link org.opencms.file.CmsObject#writePropertyObject(String, CmsProperty)}
070 * or {@link org.opencms.file.CmsObject#writePropertyObjects(String, List)}, no matter
071 * whether you want to save a new (non-existing) property, update an existing property, or delete an
072 * existing property. To delete a property you would write a property object with either the
073 * structure and/or resource record values set to {@link #DELETE_VALUE} to indicate that a
074 * property value should be deleted in the database. Set property values to null if they should
075 * remain unchanged in the database when a property object is written. As for example you want to
076 * update just the structure value of a property, you would set the structure value to the new string,
077 * and the resource value to null (which is already the case by default).<p>
078 *
079 * Use {@link #setAutoCreatePropertyDefinition(boolean)} to set a boolean flag whether a missing property
080 * definition should be created implicitly for a resource type when a property is written to the database.
081 * The default value for this flag is <code>false</code>. Thus, you receive a CmsException if you try
082 * to write a property of a resource with a resource type which lacks a property definition for
083 * this resource type. It is not a good style to set {@link #setAutoCreatePropertyDefinition(boolean)}
084 * on true to make writing properties to the database work in any case, because then you will loose
085 * control about which resource types support which property definitions.<p>
086 *
087 * @since 6.0.0
088 */
089public class CmsProperty implements Serializable, Cloneable, Comparable<CmsProperty> {
090
091    /** Transforms a given properties map, to a map where the returned values for a property are
092     * dependent on the locale.
093     */
094    public static class CmsPropertyLocaleTransformer implements Transformer {
095
096        /** The original properties map. */
097        private Map<String, String> m_properties;
098        /** The locale, w.r.t. which the properties should be accessed. */
099        private Locale m_locale;
100
101        /**
102         * Default constructor.
103         * @param properties the "raw" properties map as read for a resource.
104         * @param locale the locale w.r.t. which the properties should be accessed.
105         */
106        public CmsPropertyLocaleTransformer(Map<String, String> properties, Locale locale) {
107
108            m_properties = null == properties ? new HashMap<String, String>() : properties;
109            m_locale = null == locale ? new Locale("") : locale;
110        }
111
112        /**
113         * @see org.apache.commons.collections.Transformer#transform(java.lang.Object)
114         */
115        public Object transform(Object propertyName) {
116
117            return readProperty((String)propertyName);
118        }
119
120        /**
121         * Looks up a property in {@link #m_properties}, but returns the localized variant.
122         *
123         * @param propertyName the property to look up
124         * @return the value of the property
125         */
126        protected String readProperty(String propertyName) {
127
128            if (null == m_locale) {
129                return m_properties.get(propertyName);
130            } else {
131                return m_properties.get(getLocalizedKey(m_properties, propertyName, m_locale));
132            }
133        }
134    }
135
136    /**
137     * Signals that the resource property values of a resource
138     * should be deleted using deleteAllProperties.<p>
139     */
140    public static final int DELETE_OPTION_DELETE_RESOURCE_VALUES = 3;
141
142    /**
143     * Signals that both the structure and resource property values of a resource
144     * should be deleted using deleteAllProperties.<p>
145     */
146    public static final int DELETE_OPTION_DELETE_STRUCTURE_AND_RESOURCE_VALUES = 1;
147
148    /**
149     * Signals that the structure property values of a resource
150     * should be deleted using deleteAllProperties.<p>
151     */
152    public static final int DELETE_OPTION_DELETE_STRUCTURE_VALUES = 2;
153
154    /**
155     * An empty string to decide that a property value should be deleted when this
156     * property object is written to the database.<p>
157     */
158    public static final String DELETE_VALUE = "";
159
160    /**
161     * Value of the "mapping-type" database attribute to indicate that a property value is mapped
162     * to a resource record.<p>
163     */
164    public static final int RESOURCE_RECORD_MAPPING = 2;
165
166    /**
167     * Value of the "mapping-type" database attribute to indicate that a property value is mapped
168     * to a structure record.<p>
169     */
170    public static final int STRUCTURE_RECORD_MAPPING = 1;
171
172    /** Key used for a individual (structure) property value. */
173    public static final String TYPE_INDIVIDUAL = "individual";
174
175    /** Key used for a shared (resource) property value. */
176    public static final String TYPE_SHARED = "shared";
177
178    /** The delimiter value for separating values in a list, per default this is the <code>|</code> char. */
179    public static final char VALUE_LIST_DELIMITER = '|';
180
181    /** The list delimiter replacement String used if the delimiter itself is contained in a String value. */
182    public static final String VALUE_LIST_DELIMITER_REPLACEMENT = "%(ld)";
183
184    /** The delimiter value for separating values in a map, per default this is the <code>=</code> char. */
185    public static final char VALUE_MAP_DELIMITER = '=';
186
187    /** The map delimiter replacement String used if the delimiter itself is contained in a String value. */
188    public static final String VALUE_MAP_DELIMITER_REPLACEMENT = "%(md)";
189
190    /** The null property object to be used in caches if a property is not found. */
191    private static final CmsProperty NULL_PROPERTY = new CmsProperty();
192
193    /** Serial version UID required for safe serialization. */
194    private static final long serialVersionUID = 93613508924212782L;
195
196    /**
197     * Static initializer required for freezing the <code>{@link #NULL_PROPERTY}</code>.<p>
198     */
199    static {
200
201        NULL_PROPERTY.m_frozen = true;
202        NULL_PROPERTY.m_name = "";
203    }
204
205    /**
206     * Boolean flag to decide if the property definition for this property should be created
207     * implicitly on any write operation if doesn't exist already.<p>
208     */
209    private boolean m_autoCreatePropertyDefinition;
210
211    /** Indicates if the property is frozen (required for <code>NULL_PROPERTY</code>). */
212    private boolean m_frozen;
213
214    /** The name of this property. */
215    private String m_name;
216
217    /** The origin root path of the property. */
218    private String m_origin;
219
220    /** The value of this property attached to the resource record. */
221    private String m_resourceValue;
222
223    /** The (optional) value list of this property attached to the resource record. */
224    private List<String> m_resourceValueList;
225
226    /** The (optional) value map of this property attached to the resource record. */
227    private Map<String, String> m_resourceValueMap;
228
229    /** The value of this property attached to the structure record. */
230    private String m_structureValue;
231
232    /** The (optional) value list of this property attached to the structure record. */
233    private List<String> m_structureValueList;
234
235    /** The (optional) value map of this property attached to the structure record. */
236    private Map<String, String> m_structureValueMap;
237
238    /**
239     * Creates a new CmsProperty object.<p>
240     *
241     * The structure and resource property values are initialized to null. The structure and
242     * resource IDs are initialized to {@link org.opencms.util.CmsUUID#getNullUUID()}.<p>
243     */
244    public CmsProperty() {
245
246        // nothing to do, all values will be initialized with <code>null</code> or <code>false</code> by default
247    }
248
249    /**
250     * Creates a new CmsProperty object using the provided values.<p>
251     *
252     * If the property definition does not exist for the resource type it
253     * is automatically created when this property is written.
254     *
255     * @param name the name of the property definition
256     * @param structureValue the value to write as structure property
257     * @param resourceValue the value to write as resource property
258     */
259    public CmsProperty(String name, String structureValue, String resourceValue) {
260
261        this(name, structureValue, resourceValue, true);
262    }
263
264    /**
265     * Creates a new CmsProperty object using the provided values.<p>
266     *
267     * If <code>null</code> is supplied for the resource or structure value, this
268     * value will not be available for this property.<p>
269     *
270     * @param name the name of the property definition
271     * @param structureValue the value to write as structure property, or <code>null</code>
272     * @param resourceValue the value to write as resource property , or <code>null</code>
273     * @param autoCreatePropertyDefinition if <code>true</code>, the property definition for this property will be
274     *      created implicitly on any write operation if it doesn't exist already
275     */
276    public CmsProperty(String name, String structureValue, String resourceValue, boolean autoCreatePropertyDefinition) {
277
278        m_name = name.trim();
279        m_structureValue = structureValue;
280        m_resourceValue = resourceValue;
281        m_autoCreatePropertyDefinition = autoCreatePropertyDefinition;
282    }
283
284    /**
285     * Searches in a list for the first occurrence of a {@link CmsProperty} object with the given name.<p>
286     *
287     * To check if the "null property" has been returned if a property was
288     * not found, use {@link #isNullProperty()} on the result.<p>
289     *
290     * @param name a property name
291     * @param list a list of {@link CmsProperty} objects
292     * @return the index of the first occurrence of the name in they specified list,
293     *      or {@link CmsProperty#getNullProperty()} if the name is not found
294     */
295    public static final CmsProperty get(String name, List<CmsProperty> list) {
296
297        CmsProperty property = null;
298        name = name.trim();
299        // choose the fastest method to traverse the list
300        if (list instanceof RandomAccess) {
301            for (int i = 0, n = list.size(); i < n; i++) {
302                property = list.get(i);
303                if (property.m_name.equals(name)) {
304                    return property;
305                }
306            }
307        } else {
308            Iterator<CmsProperty> i = list.iterator();
309            while (i.hasNext()) {
310                property = i.next();
311                if (property.m_name.equals(name)) {
312                    return property;
313                }
314            }
315        }
316
317        return NULL_PROPERTY;
318    }
319
320    /**
321     * Returns the value for the best matching local-specific property version.
322     *
323     * @param propertiesMap the "raw" property map
324     * @param key the name of the property to search for
325     * @param locale the locale to search for
326     *
327     * @return the key for the best matching local-specific property version.
328     */
329    public static String getLocaleSpecificPropertyValue(
330        Map<String, CmsProperty> propertiesMap,
331        String key,
332        Locale locale) {
333
334        String localeSpecificKey = CmsProperty.getLocalizedKey(propertiesMap, key, locale);
335        if (propertiesMap.containsKey(localeSpecificKey)) {
336            return propertiesMap.get(localeSpecificKey).getValue();
337        }
338        return null;
339    }
340
341    /**
342     * Returns the key for the best matching local-specific property version.
343     *
344     * @param propertiesMap the "raw" property map
345     * @param key the name of the property to search for
346     * @param locale the locale to search for
347     *
348     * @return the key for the best matching local-specific property version.
349     */
350    public static String getLocalizedKey(Map<String, ?> propertiesMap, String key, Locale locale) {
351
352        List<String> localizedKeys = CmsLocaleManager.getLocaleVariants(key, locale, true, false);
353        for (String localizedKey : localizedKeys) {
354            if (propertiesMap.containsKey(localizedKey)) {
355                return localizedKey;
356            }
357        }
358        return key;
359    }
360
361    /**
362     * Returns the null property object.<p>
363     *
364     * @return the null property object
365     */
366    public static final CmsProperty getNullProperty() {
367
368        return NULL_PROPERTY;
369    }
370
371    /**
372     * Transforms a list of CmsProperty objects with structure and resource values into a map with
373     * CmsProperty object values keyed by property keys.<p>
374     *
375     * @param list a list of CmsProperty objects
376     * @return a map with CmsPropery object values keyed by property keys
377     */
378    public static Map<String, CmsProperty> getPropertyMap(List<CmsProperty> list) {
379
380        Map<String, CmsProperty> result = null;
381        String key = null;
382        CmsProperty property = null;
383
384        if ((list == null) || (list.size() == 0)) {
385            return Collections.emptyMap();
386        }
387
388        result = new HashMap<String, CmsProperty>();
389
390        // choose the fastest method to iterate the list
391        if (list instanceof RandomAccess) {
392            for (int i = 0, n = list.size(); i < n; i++) {
393                property = list.get(i);
394                key = property.getName();
395                result.put(key, property);
396            }
397        } else {
398            Iterator<CmsProperty> i = list.iterator();
399            while (i.hasNext()) {
400                property = i.next();
401                key = property.getName();
402                result.put(key, property);
403            }
404        }
405
406        return result;
407    }
408
409    /**
410     * Calls <code>{@link #setAutoCreatePropertyDefinition(boolean)}</code> for each
411     * property object in the given List with the given <code>value</code> parameter.<p>
412     *
413     * This method will modify the objects in the input list directly.<p>
414     *
415     * @param list a list of {@link CmsProperty} objects to modify
416     * @param value boolean value
417     *
418     * @return the modified list of {@link CmsProperty} objects
419     *
420     * @see #setAutoCreatePropertyDefinition(boolean)
421     */
422    public static final List<CmsProperty> setAutoCreatePropertyDefinitions(List<CmsProperty> list, boolean value) {
423
424        CmsProperty property;
425
426        // choose the fastest method to traverse the list
427        if (list instanceof RandomAccess) {
428            for (int i = 0, n = list.size(); i < n; i++) {
429                property = list.get(i);
430                property.m_autoCreatePropertyDefinition = value;
431            }
432        } else {
433            Iterator<CmsProperty> i = list.iterator();
434            while (i.hasNext()) {
435                property = i.next();
436                property.m_autoCreatePropertyDefinition = value;
437            }
438        }
439
440        return list;
441    }
442
443    /**
444     * Calls <code>{@link #setFrozen(boolean)}</code> for each
445     * {@link CmsProperty} object in the given List if it is not already frozen.<p>
446     *
447     * This method will modify the objects in the input list directly.<p>
448     *
449     * @param list a list of {@link CmsProperty} objects
450     *
451     * @return the modified list of properties
452     *
453     * @see #setFrozen(boolean)
454     */
455    public static final List<CmsProperty> setFrozen(List<CmsProperty> list) {
456
457        CmsProperty property;
458
459        // choose the fastest method to traverse the list
460        if (list instanceof RandomAccess) {
461            for (int i = 0, n = list.size(); i < n; i++) {
462                property = list.get(i);
463                if (!property.isFrozen()) {
464                    property.setFrozen(true);
465                }
466            }
467        } else {
468            Iterator<CmsProperty> i = list.iterator();
469            while (i.hasNext()) {
470                property = i.next();
471                if (!property.isFrozen()) {
472                    property.setFrozen(true);
473                }
474            }
475        }
476
477        return list;
478    }
479
480    /**
481     * Transforms a Map of String values into a list of
482     * {@link CmsProperty} objects with the property name set from the
483     * Map key, and the structure value set from the Map value.<p>
484     *
485     * @param map a Map with String keys and String values
486     *
487     * @return a list of {@link CmsProperty} objects
488     */
489    public static List<CmsProperty> toList(Map<String, String> map) {
490
491        if ((map == null) || (map.size() == 0)) {
492            return Collections.emptyList();
493        }
494
495        List<CmsProperty> result = new ArrayList<CmsProperty>(map.size());
496        Iterator<Map.Entry<String, String>> i = map.entrySet().iterator();
497        while (i.hasNext()) {
498            Map.Entry<String, String> e = i.next();
499            CmsProperty property = new CmsProperty(e.getKey(), e.getValue(), null);
500            result.add(property);
501        }
502
503        return result;
504    }
505
506    /**
507     * Transforms a list of {@link CmsProperty} objects into a Map which uses the property name as
508     * Map key (String), and the property value as Map value (String).<p>
509     *
510     * @param list a list of {@link CmsProperty} objects
511     *
512     * @return a Map which uses the property names as
513     *      Map keys (String), and the property values as Map values (String)
514     */
515    public static Map<String, String> toMap(List<CmsProperty> list) {
516
517        if ((list == null) || (list.size() == 0)) {
518            return Collections.emptyMap();
519        }
520
521        String name = null;
522        String value = null;
523        CmsProperty property = null;
524        Map<String, String> result = new HashMap<String, String>(list.size());
525
526        // choose the fastest method to traverse the list
527        if (list instanceof RandomAccess) {
528            for (int i = 0, n = list.size(); i < n; i++) {
529                property = list.get(i);
530                name = property.m_name;
531                value = property.getValue();
532                result.put(name, value);
533            }
534        } else {
535            Iterator<CmsProperty> i = list.iterator();
536            while (i.hasNext()) {
537                property = i.next();
538                name = property.m_name;
539                value = property.getValue();
540                result.put(name, value);
541            }
542        }
543
544        return result;
545    }
546
547    /**
548     * Stores a collection of properties in a map, with the property names as keys.<p>
549     *
550     * @param properties the properties to store in the map
551     *
552     * @return the map with the property names as keys and the property objects as values
553     */
554    public static Map<String, CmsProperty> toObjectMap(Iterable<CmsProperty> properties) {
555
556        Map<String, CmsProperty> result = new LinkedHashMap<String, CmsProperty>();
557        for (CmsProperty property : properties) {
558            result.put(property.getName(), property);
559        }
560        return result;
561    }
562
563    /**
564     * Wraps a null value into a null property, and returns all other values unchanged.<p>
565     *
566     * @param prop the value to wrap
567     *
568     * @return a wrapped null property, or the original prop if it wasn't null
569     */
570    public static CmsProperty wrapIfNull(CmsProperty prop) {
571
572        if (prop == null) {
573            return getNullProperty();
574        } else {
575            return prop;
576        }
577    }
578
579    /**
580     * Checks if the property definition for this property will be
581     * created implicitly on any write operation if doesn't already exist.<p>
582     *
583     * @return <code>true</code>, if the property definition for this property will be created implicitly on any write operation
584     */
585    public boolean autoCreatePropertyDefinition() {
586
587        return m_autoCreatePropertyDefinition;
588    }
589
590    /**
591     * Creates a clone of this property.<p>
592     *
593     * @return a clone of this property
594     *
595     * @see #cloneAsProperty()
596     */
597    @Override
598    public CmsProperty clone() {
599
600        return cloneAsProperty();
601    }
602
603    /**
604     * Creates a clone of this property that already is of type <code>{@link CmsProperty}</code>.<p>
605     *
606     * The cloned property will not be frozen.<p>
607     *
608     * @return a clone of this property that already is of type <code>{@link CmsProperty}</code>
609     */
610    public CmsProperty cloneAsProperty() {
611
612        if (this == NULL_PROPERTY) {
613            // null property must never be cloned
614            return NULL_PROPERTY;
615        }
616        CmsProperty clone = new CmsProperty();
617        clone.m_name = m_name;
618        clone.m_structureValue = m_structureValue;
619        clone.m_structureValueList = m_structureValueList;
620        clone.m_resourceValue = m_resourceValue;
621        clone.m_resourceValueList = m_resourceValueList;
622        clone.m_autoCreatePropertyDefinition = m_autoCreatePropertyDefinition;
623        clone.m_origin = m_origin;
624        // the value for m_frozen does not need to be set as it is false by default
625
626        return clone;
627    }
628
629    /**
630     * Compares this property to another Object.<p>
631     *
632     * @param obj the other object to be compared
633     * @return if the argument is a property object, returns zero if the name of the argument is equal to the name of this property object,
634     *      a value less than zero if the name of this property is lexicographically less than the name of the argument,
635     *      or a value greater than zero if the name of this property is lexicographically greater than the name of the argument
636     */
637    public int compareTo(CmsProperty obj) {
638
639        if (obj == this) {
640            return 0;
641        }
642        return m_name.compareTo(obj.m_name);
643    }
644
645    /**
646     * Tests if a specified object is equal to this CmsProperty object.<p>
647     *
648     * Two property objects are equal if their names are equal.<p>
649     *
650     * In case you want to compare the values as well as the name,
651     * use {@link #isIdentical(CmsProperty)} instead.<p>
652     *
653     * @param obj another object
654     * @return true, if the specified object is equal to this CmsProperty object
655     *
656     * @see #isIdentical(CmsProperty)
657     */
658    @Override
659    public boolean equals(Object obj) {
660
661        if (obj == this) {
662            return true;
663        }
664        if (obj instanceof CmsProperty) {
665            return ((CmsProperty)obj).m_name.equals(m_name);
666        }
667        return false;
668    }
669
670    /**
671     * Returns the name of this property.<p>
672     *
673     * @return the name of this property
674     */
675    public String getName() {
676
677        return m_name;
678    }
679
680    /**
681     * Returns the root path of the resource from which the property was read.<p>
682     *
683     * @return the root path of the resource from which the property was read
684     */
685    public String getOrigin() {
686
687        return m_origin;
688    }
689
690    /**
691     * Returns the value of this property attached to the resource record.<p>
692     *
693     * @return the value of this property attached to the resource record
694     */
695    public String getResourceValue() {
696
697        return m_resourceValue;
698    }
699
700    /**
701     * Returns the value of this property attached to the resource record, split as a list.<p>
702     *
703     * This list is build form the resource value, which is split into separate values
704     * using the <code>|</code> char as delimiter. If the delimiter is not found,
705     * then the list will contain one entry which is equal to <code>{@link #getResourceValue()}</code>.<p>
706     *
707     * @return the value of this property attached to the resource record, split as a (unmodifiable) list of Strings
708     */
709    public List<String> getResourceValueList() {
710
711        if ((m_resourceValueList == null) && (m_resourceValue != null)) {
712            // use lazy initializing of the list
713            m_resourceValueList = createListFromValue(m_resourceValue);
714            m_resourceValueList = Collections.unmodifiableList(m_resourceValueList);
715        }
716        return m_resourceValueList;
717    }
718
719    /**
720     * Returns the value of this property attached to the resource record as a map.<p>
721     *
722     * This map is build from the used value, which is split into separate key/value pairs
723     * using the <code>|</code> char as delimiter. If the delimiter is not found,
724     * then the map will contain one entry.<p>
725     *
726     * The key/value pairs are separated with the <code>=</code>.<p>
727     *
728     * @return the value of this property attached to the resource record, as an (unmodifiable) map of Strings
729     */
730    public Map<String, String> getResourceValueMap() {
731
732        if ((m_resourceValueMap == null) && (m_resourceValue != null)) {
733            // use lazy initializing of the map
734            m_resourceValueMap = createMapFromValue(m_resourceValue);
735            m_resourceValueMap = Collections.unmodifiableMap(m_resourceValueMap);
736        }
737        return m_resourceValueMap;
738    }
739
740    /**
741     * Returns the value of this property attached to the structure record.<p>
742     *
743     * @return the value of this property attached to the structure record
744     */
745    public String getStructureValue() {
746
747        return m_structureValue;
748    }
749
750    /**
751     * Returns the value of this property attached to the structure record, split as a list.<p>
752     *
753     * This list is build form the structure value, which is split into separate values
754     * using the <code>|</code> char as delimiter. If the delimiter is not found,
755     * then the list will contain one entry which is equal to <code>{@link #getStructureValue()}</code>.<p>
756     *
757     * @return the value of this property attached to the structure record, split as a (unmodifiable) list of Strings
758     */
759    public List<String> getStructureValueList() {
760
761        if ((m_structureValueList == null) && (m_structureValue != null)) {
762            // use lazy initializing of the list
763            m_structureValueList = createListFromValue(m_structureValue);
764            m_structureValueList = Collections.unmodifiableList(m_structureValueList);
765        }
766        return m_structureValueList;
767    }
768
769    /**
770     * Returns the value of this property attached to the structure record as a map.<p>
771     *
772     * This map is build from the used value, which is split into separate key/value pairs
773     * using the <code>|</code> char as delimiter. If the delimiter is not found,
774     * then the map will contain one entry.<p>
775     *
776     * The key/value pairs are separated with the <code>=</code>.<p>
777     *
778     * @return the value of this property attached to the structure record, as an (unmodifiable) map of Strings
779     */
780    public Map<String, String> getStructureValueMap() {
781
782        if ((m_structureValueMap == null) && (m_structureValue != null)) {
783            // use lazy initializing of the map
784            m_structureValueMap = createMapFromValue(m_structureValue);
785            m_structureValueMap = Collections.unmodifiableMap(m_structureValueMap);
786        }
787        return m_structureValueMap;
788    }
789
790    /**
791     * Returns the compound value of this property.<p>
792     *
793     * The value returned is the value of {@link #getStructureValue()}, if it is not <code>null</code>.
794     * Otherwise the value if {@link #getResourceValue()} is returned (which may also be <code>null</code>).<p>
795     *
796     * @return the compound value of this property
797     */
798    public String getValue() {
799
800        return (m_structureValue != null) ? m_structureValue : m_resourceValue;
801    }
802
803    /**
804     * Returns the compound value of this property, or a specified default value,
805     * if both the structure and resource values are null.<p>
806     *
807     * In other words, this method returns the defaultValue if this property object
808     * is the null property (see {@link CmsProperty#getNullProperty()}).<p>
809     *
810     * @param defaultValue a default value which is returned if both the structure and resource values are <code>null</code>
811     *
812     * @return the compound value of this property, or the default value
813     */
814    public String getValue(String defaultValue) {
815
816        if (this == CmsProperty.NULL_PROPERTY) {
817            // return the default value if this property is the null property
818            return defaultValue;
819        }
820
821        // somebody might have set both values to null manually
822        // on a property object different from the null property...
823        return (m_structureValue != null)
824        ? m_structureValue
825        : ((m_resourceValue != null) ? m_resourceValue : defaultValue);
826    }
827
828    /**
829     * Returns the compound value of this property, split as a list.<p>
830     *
831     * This list is build form the used value, which is split into separate values
832     * using the <code>|</code> char as delimiter. If the delimiter is not found,
833     * then the list will contain one entry.<p>
834     *
835     * The value returned is the value of {@link #getStructureValueList()}, if it is not <code>null</code>.
836     * Otherwise the value of {@link #getResourceValueList()} is returned (which may also be <code>null</code>).<p>
837     *
838     * @return the compound value of this property, split as a (unmodifiable) list of Strings
839     */
840    public List<String> getValueList() {
841
842        return (m_structureValue != null) ? getStructureValueList() : getResourceValueList();
843    }
844
845    /**
846     * Returns the compound value of this property, split as a list, or a specified default value list,
847     * if both the structure and resource values are null.<p>
848     *
849     * In other words, this method returns the defaultValue if this property object
850     * is the null property (see {@link CmsProperty#getNullProperty()}).<p>
851     *
852     * @param defaultValue a default value list which is returned if both the structure and resource values are <code>null</code>
853     *
854     * @return the compound value of this property, split as a (unmodifiable) list of Strings
855     */
856    public List<String> getValueList(List<String> defaultValue) {
857
858        if (this == CmsProperty.NULL_PROPERTY) {
859            // return the default value if this property is the null property
860            return defaultValue;
861        }
862
863        // somebody might have set both values to null manually
864        // on a property object different from the null property...
865        return (m_structureValue != null)
866        ? getStructureValueList()
867        : ((m_resourceValue != null) ? getResourceValueList() : defaultValue);
868    }
869
870    /**
871     * Returns the compound value of this property as a map.<p>
872     *
873     * This map is build from the used value, which is split into separate key/value pairs
874     * using the <code>|</code> char as delimiter. If the delimiter is not found,
875     * then the map will contain one entry.<p>
876     *
877     * The key/value pairs are separated with the <code>=</code>.<p>
878     *
879     * The value returned is the value of {@link #getStructureValueMap()}, if it is not <code>null</code>.
880     * Otherwise the value of {@link #getResourceValueMap()} is returned (which may also be <code>null</code>).<p>
881     *
882     * @return the compound value of this property as a (unmodifiable) map of Strings
883     */
884    public Map<String, String> getValueMap() {
885
886        return (m_structureValue != null) ? getStructureValueMap() : getResourceValueMap();
887    }
888
889    /**
890     * Returns the compound value of this property as a map, or a specified default value map,
891     * if both the structure and resource values are null.<p>
892     *
893     * In other words, this method returns the defaultValue if this property object
894     * is the null property (see {@link CmsProperty#getNullProperty()}).<p>
895     *
896     * @param defaultValue a default value map which is returned if both the structure and resource values are <code>null</code>
897     *
898     * @return the compound value of this property as a (unmodifiable) map of Strings
899     */
900    public Map<String, String> getValueMap(Map<String, String> defaultValue) {
901
902        if (this == CmsProperty.NULL_PROPERTY) {
903            // return the default value if this property is the null property
904            return defaultValue;
905        }
906
907        // somebody might have set both values to null manually
908        // on a property object different from the null property...
909        return (m_structureValue != null)
910        ? getStructureValueMap()
911        : ((m_resourceValue != null) ? getResourceValueMap() : defaultValue);
912    }
913
914    /**
915     * Returns the hash code of the property, which is based only on the property name, not on the values.<p>
916     *
917     * The resource and structure values are not taken into consideration for the hashcode generation
918     * because the {@link #equals(Object)} implementation also does not take these into consideration.<p>
919     *
920     * @return the hash code of the property
921     *
922     * @see java.lang.Object#hashCode()
923     */
924    @Override
925    public int hashCode() {
926
927        return m_name.hashCode();
928    }
929
930    /**
931     * Checks if the resource value of this property should be deleted when this
932     * property object is written to the database.<p>
933     *
934     * @return true, if the resource value of this property should be deleted
935     * @see CmsProperty#DELETE_VALUE
936     */
937    public boolean isDeleteResourceValue() {
938
939        return (m_resourceValue == DELETE_VALUE) || ((m_resourceValue != null) && (m_resourceValue.length() == 0));
940    }
941
942    /**
943     * Checks if the structure value of this property should be deleted when this
944     * property object is written to the database.<p>
945     *
946     * @return true, if the structure value of this property should be deleted
947     * @see CmsProperty#DELETE_VALUE
948     */
949    public boolean isDeleteStructureValue() {
950
951        return (m_structureValue == DELETE_VALUE) || ((m_structureValue != null) && (m_structureValue.length() == 0));
952    }
953
954    /**
955     * Returns <code>true</code> if this property is frozen, that is read only.<p>
956     *
957     * @return <code>true</code> if this property is frozen, that is read only
958     */
959    public boolean isFrozen() {
960
961        return m_frozen;
962    }
963
964    /**
965     * Tests if a given CmsProperty is identical to this CmsProperty object.<p>
966     *
967     * The property object are identical if their name, structure and
968     * resource values are all equals.<p>
969     *
970     * @param property another property object
971     * @return true, if the specified object is equal to this CmsProperty object
972     */
973    public boolean isIdentical(CmsProperty property) {
974
975        boolean isEqual;
976
977        // compare the name
978        if (m_name == null) {
979            isEqual = (property.getName() == null);
980        } else {
981            isEqual = m_name.equals(property.getName());
982        }
983
984        // compare the structure value
985        if (m_structureValue == null) {
986            isEqual &= (property.getStructureValue() == null);
987        } else {
988            isEqual &= m_structureValue.equals(property.getStructureValue());
989        }
990
991        // compare the resource value
992        if (m_resourceValue == null) {
993            isEqual &= (property.getResourceValue() == null);
994        } else {
995            isEqual &= m_resourceValue.equals(property.getResourceValue());
996        }
997
998        return isEqual;
999    }
1000
1001    /**
1002     * Checks if this property object is the null property object.<p>
1003     *
1004     * @return true if this property object is the null property object
1005     */
1006    public boolean isNullProperty() {
1007
1008        return NULL_PROPERTY.equals(this);
1009    }
1010
1011    /**
1012     * Sets the boolean flag to decide if the property definition for this property should be
1013     * created implicitly on any write operation if doesn't exist already.<p>
1014     *
1015     * @param value true, if the property definition for this property should be created implicitly on any write operation
1016     */
1017    public void setAutoCreatePropertyDefinition(boolean value) {
1018
1019        checkFrozen();
1020        m_autoCreatePropertyDefinition = value;
1021    }
1022
1023    /**
1024     * Sets the frozen state of the property, if set to <code>true</code> then this property is read only.<p>
1025     *
1026     * If the property is already frozen, then setting the frozen state to <code>true</code> again is allowed,
1027     * but setting the value to <code>false</code> causes a <code>{@link CmsRuntimeException}</code>.<p>
1028     *
1029     * @param frozen the frozen state to set
1030     */
1031    public void setFrozen(boolean frozen) {
1032
1033        if (!frozen) {
1034            checkFrozen();
1035        }
1036        m_frozen = frozen;
1037    }
1038
1039    /**
1040     * Sets the name of this property.<p>
1041     *
1042     * @param name the name to set
1043     */
1044    public void setName(String name) {
1045
1046        checkFrozen();
1047        m_name = name.trim();
1048    }
1049
1050    /**
1051     * Sets the path of the resource from which the property was read.<p>
1052     *
1053     * @param originRootPath the root path of the root path from which the property was read
1054     */
1055    public void setOrigin(String originRootPath) {
1056
1057        checkFrozen();
1058        m_origin = originRootPath;
1059    }
1060
1061    /**
1062     * Sets the value of this property attached to the resource record.<p>
1063     *
1064     * @param resourceValue the value of this property attached to the resource record
1065     */
1066    public void setResourceValue(String resourceValue) {
1067
1068        checkFrozen();
1069        m_resourceValue = resourceValue;
1070        m_resourceValueList = null;
1071    }
1072
1073    /**
1074     * Sets the value of this property attached to the resource record from the given list of Strings.<p>
1075     *
1076     * The value will be created from the individual values of the given list, which are appended
1077     * using the <code>|</code> char as delimiter.<p>
1078     *
1079     * @param valueList the list of value (Strings) to attach to the resource record
1080     */
1081    public void setResourceValueList(List<String> valueList) {
1082
1083        checkFrozen();
1084        if (valueList != null) {
1085            m_resourceValueList = new ArrayList<String>(valueList);
1086            m_resourceValueList = Collections.unmodifiableList(m_resourceValueList);
1087            m_resourceValue = createValueFromList(m_resourceValueList);
1088        } else {
1089            m_resourceValueList = null;
1090            m_resourceValue = null;
1091        }
1092    }
1093
1094    /**
1095     * Sets the value of this property attached to the resource record from the given map of Strings.<p>
1096     *
1097     * The value will be created from the individual values of the given map, which are appended
1098     * using the <code>|</code> char as delimiter, the map keys and values are separated by a <code>=</code>.<p>
1099     *
1100     * @param valueMap the map of key/value (Strings) to attach to the resource record
1101     */
1102    public void setResourceValueMap(Map<String, String> valueMap) {
1103
1104        checkFrozen();
1105        if (valueMap != null) {
1106            m_resourceValueMap = new HashMap<String, String>(valueMap);
1107            m_resourceValueMap = Collections.unmodifiableMap(m_resourceValueMap);
1108            m_resourceValue = createValueFromMap(m_resourceValueMap);
1109        } else {
1110            m_resourceValueMap = null;
1111            m_resourceValue = null;
1112        }
1113    }
1114
1115    /**
1116     * Sets the value of this property attached to the structure record.<p>
1117     *
1118     * @param structureValue the value of this property attached to the structure record
1119     */
1120    public void setStructureValue(String structureValue) {
1121
1122        checkFrozen();
1123        m_structureValue = structureValue;
1124        m_structureValueList = null;
1125    }
1126
1127    /**
1128     * Sets the value of this property attached to the structure record from the given list of Strings.<p>
1129     *
1130     * The value will be created from the individual values of the given list, which are appended
1131     * using the <code>|</code> char as delimiter.<p>
1132     *
1133     * @param valueList the list of value (Strings) to attach to the structure record
1134     */
1135    public void setStructureValueList(List<String> valueList) {
1136
1137        checkFrozen();
1138        if (valueList != null) {
1139            m_structureValueList = new ArrayList<String>(valueList);
1140            m_structureValueList = Collections.unmodifiableList(m_structureValueList);
1141            m_structureValue = createValueFromList(m_structureValueList);
1142        } else {
1143            m_structureValueList = null;
1144            m_structureValue = null;
1145        }
1146    }
1147
1148    /**
1149     * Sets the value of this property attached to the structure record from the given map of Strings.<p>
1150     *
1151     * The value will be created from the individual values of the given map, which are appended
1152     * using the <code>|</code> char as delimiter, the map keys and values are separated by a <code>=</code>.<p>
1153     *
1154     * @param valueMap the map of key/value (Strings) to attach to the structure record
1155     */
1156    public void setStructureValueMap(Map<String, String> valueMap) {
1157
1158        checkFrozen();
1159        if (valueMap != null) {
1160            m_structureValueMap = new HashMap<String, String>(valueMap);
1161            m_structureValueMap = Collections.unmodifiableMap(m_structureValueMap);
1162            m_structureValue = createValueFromMap(m_structureValueMap);
1163        } else {
1164            m_structureValueMap = null;
1165            m_structureValue = null;
1166        }
1167    }
1168
1169    /**
1170     * Sets the value of this property as either shared or
1171     * individual value.<p>
1172     *
1173     * If the given type equals {@link CmsProperty#TYPE_SHARED} then
1174     * the value is set as a shared (resource) value, otherwise it
1175     * is set as individual (structure) value.<p>
1176     *
1177     * @param value the value to set
1178     * @param type the value type to set
1179     */
1180    public void setValue(String value, String type) {
1181
1182        checkFrozen();
1183        setAutoCreatePropertyDefinition(true);
1184        if (TYPE_SHARED.equalsIgnoreCase(type)) {
1185            // set the provided value as shared (resource) value
1186            setResourceValue(value);
1187        } else {
1188            // set the provided value as individual (structure) value
1189            setStructureValue(value);
1190        }
1191    }
1192
1193    /**
1194     * Returns a string representation of this property object.<p>
1195     *
1196     * @see java.lang.Object#toString()
1197     */
1198    @Override
1199    public String toString() {
1200
1201        StringBuffer strBuf = new StringBuffer();
1202
1203        strBuf.append("[").append(getClass().getName()).append(": ");
1204        strBuf.append("name: '").append(m_name).append("'");
1205        strBuf.append(", value: '").append(getValue()).append("'");
1206        strBuf.append(", structure value: '").append(m_structureValue).append("'");
1207        strBuf.append(", resource value: '").append(m_resourceValue).append("'");
1208        strBuf.append(", frozen: ").append(m_frozen);
1209        strBuf.append(", origin: ").append(m_origin);
1210        strBuf.append("]");
1211
1212        return strBuf.toString();
1213    }
1214
1215    /**
1216     * Checks if this property is frozen, that is read only.<p>
1217     */
1218    private void checkFrozen() {
1219
1220        if (m_frozen) {
1221            throw new CmsRuntimeException(Messages.get().container(Messages.ERR_PROPERTY_FROZEN_1, toString()));
1222        }
1223    }
1224
1225    /**
1226     * Returns the list value representation for the given String.<p>
1227     *
1228     * The given value is split along the <code>|</code> char.<p>
1229     *
1230     * @param value the value to create the list representation for
1231     *
1232     * @return the list value representation for the given String
1233     */
1234    private List<String> createListFromValue(String value) {
1235
1236        if (value == null) {
1237            return null;
1238        }
1239        List<String> result = CmsStringUtil.splitAsList(value, VALUE_LIST_DELIMITER);
1240        if (value.indexOf(VALUE_LIST_DELIMITER_REPLACEMENT) != -1) {
1241            List<String> tempList = new ArrayList<String>(result.size());
1242            Iterator<String> i = result.iterator();
1243            while (i.hasNext()) {
1244                String item = i.next();
1245                tempList.add(rebuildDelimiter(item, VALUE_LIST_DELIMITER, VALUE_LIST_DELIMITER_REPLACEMENT));
1246            }
1247            result = tempList;
1248        }
1249
1250        return result;
1251    }
1252
1253    /**
1254     * Returns the map value representation for the given String.<p>
1255     *
1256     * The given value is split along the <code>|</code> char, the map keys and values are separated by a <code>=</code>.<p>
1257     *
1258     * @param value the value to create the map representation for
1259     *
1260     * @return the map value representation for the given String
1261     */
1262    private Map<String, String> createMapFromValue(String value) {
1263
1264        if (value == null) {
1265            return null;
1266        }
1267        List<String> entries = createListFromValue(value);
1268        Iterator<String> i = entries.iterator();
1269        Map<String, String> result = new HashMap<String, String>(entries.size());
1270        boolean rebuildDelimiters = false;
1271        if (value.indexOf(VALUE_MAP_DELIMITER_REPLACEMENT) != -1) {
1272            rebuildDelimiters = true;
1273        }
1274        while (i.hasNext()) {
1275            String entry = i.next();
1276            int index = entry.indexOf(VALUE_MAP_DELIMITER);
1277            if (index != -1) {
1278                String key = entry.substring(0, index);
1279                String val = "";
1280                if ((index + 1) < entry.length()) {
1281                    val = entry.substring(index + 1);
1282                }
1283                if (CmsStringUtil.isNotEmpty(key)) {
1284                    if (rebuildDelimiters) {
1285                        key = rebuildDelimiter(key, VALUE_MAP_DELIMITER, VALUE_MAP_DELIMITER_REPLACEMENT);
1286                        val = rebuildDelimiter(val, VALUE_MAP_DELIMITER, VALUE_MAP_DELIMITER_REPLACEMENT);
1287                    }
1288                    result.put(key, val);
1289                }
1290            }
1291        }
1292        return result;
1293    }
1294
1295    /**
1296     * Returns the single String value representation for the given value list.<p>
1297     *
1298     * @param valueList the value list to create the single String value for
1299     *
1300     * @return the single String value representation for the given value list
1301     */
1302    private String createValueFromList(List<String> valueList) {
1303
1304        if (valueList == null) {
1305            return null;
1306        }
1307        StringBuffer result = new StringBuffer(valueList.size() * 32);
1308        Iterator<String> i = valueList.iterator();
1309        while (i.hasNext()) {
1310            result.append(
1311                replaceDelimiter(i.next().toString(), VALUE_LIST_DELIMITER, VALUE_LIST_DELIMITER_REPLACEMENT));
1312            if (i.hasNext()) {
1313                result.append(VALUE_LIST_DELIMITER);
1314            }
1315        }
1316        return result.toString();
1317    }
1318
1319    /**
1320     * Returns the single String value representation for the given value map.<p>
1321     *
1322     * @param valueMap the value map to create the single String value for
1323     *
1324     * @return the single String value representation for the given value map
1325     */
1326    private String createValueFromMap(Map<String, String> valueMap) {
1327
1328        if (valueMap == null) {
1329            return null;
1330        }
1331        StringBuffer result = new StringBuffer(valueMap.size() * 32);
1332        Iterator<Map.Entry<String, String>> i = valueMap.entrySet().iterator();
1333        while (i.hasNext()) {
1334            Map.Entry<String, String> entry = i.next();
1335            String key = entry.getKey();
1336            String value = entry.getValue();
1337            key = replaceDelimiter(key, VALUE_LIST_DELIMITER, VALUE_LIST_DELIMITER_REPLACEMENT);
1338            key = replaceDelimiter(key, VALUE_MAP_DELIMITER, VALUE_MAP_DELIMITER_REPLACEMENT);
1339            value = replaceDelimiter(value, VALUE_LIST_DELIMITER, VALUE_LIST_DELIMITER_REPLACEMENT);
1340            value = replaceDelimiter(value, VALUE_MAP_DELIMITER, VALUE_MAP_DELIMITER_REPLACEMENT);
1341            result.append(key);
1342            result.append(VALUE_MAP_DELIMITER);
1343            result.append(value);
1344            if (i.hasNext()) {
1345                result.append(VALUE_LIST_DELIMITER);
1346            }
1347        }
1348        return result.toString();
1349    }
1350
1351    /**
1352     * Rebuilds the given delimiter character from the replacement string.<p>
1353     *
1354     * @param value the string that is scanned
1355     * @param delimiter the delimiter character to rebuild
1356     * @param delimiterReplacement the replacement string for the delimiter character
1357     * @return the substituted string
1358     */
1359    private String rebuildDelimiter(String value, char delimiter, String delimiterReplacement) {
1360
1361        return CmsStringUtil.substitute(value, delimiterReplacement, String.valueOf(delimiter));
1362    }
1363
1364    /**
1365     * Replaces the given delimiter character with the replacement string.<p>
1366     *
1367     * @param value the string that is scanned
1368     * @param delimiter the delimiter character to replace
1369     * @param delimiterReplacement the replacement string for the delimiter character
1370     * @return the substituted string
1371     */
1372    private String replaceDelimiter(String value, char delimiter, String delimiterReplacement) {
1373
1374        return CmsStringUtil.substitute(value, String.valueOf(delimiter), delimiterReplacement);
1375    }
1376
1377}