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.xml.types;
029
030import org.opencms.file.CmsObject;
031import org.opencms.i18n.CmsEncoder;
032import org.opencms.main.CmsLog;
033import org.opencms.main.CmsRuntimeException;
034import org.opencms.relations.CmsRelationType;
035import org.opencms.util.CmsFileUtil;
036import org.opencms.util.CmsStringUtil;
037import org.opencms.widgets.I_CmsWidgetParameter;
038import org.opencms.xml.CmsXmlContentDefinition;
039import org.opencms.xml.CmsXmlGenericWrapper;
040import org.opencms.xml.CmsXmlUtils;
041import org.opencms.xml.I_CmsXmlDocument;
042
043import java.util.List;
044import java.util.Locale;
045
046import org.apache.commons.logging.Log;
047
048import org.dom4j.Element;
049
050/**
051 * Base class for XML content value implementations.<p>
052 *
053 * @since 6.0.0
054 */
055public abstract class A_CmsXmlContentValue implements I_CmsXmlContentValue, I_CmsWidgetParameter {
056
057    /** The log object for this class. */
058    private static final Log LOG = CmsLog.getLog(A_CmsXmlContentValue.class);
059
060    /** The default value for nodes of this value. */
061    protected String m_defaultValue;
062
063    /** The XML content instance this value belongs to. */
064    protected I_CmsXmlDocument m_document;
065
066    /** The XML element node that contains this value. */
067    protected Element m_element;
068
069    /** The locale this value was generated for. */
070    protected Locale m_locale;
071
072    /** The maximum occurrences of this value according to the parent schema. */
073    protected int m_maxOccurs;
074
075    /** The minimum occurrences of this value according to the parent schema. */
076    protected int m_minOccurs;
077
078    /** The configured XML node name of this value. */
079    protected String m_name;
080
081    /** The content definition this schema type belongs to. */
082    private CmsXmlContentDefinition m_contentDefinition;
083
084    /** The index position of this content value, with special handling for choice groups. */
085    private int m_index;
086
087    /** The maximum index of this type that currently exist in the source document. */
088    private int m_maxIndex;
089
090    /** Optional localized key prefix identifier. */
091    private String m_prefix;
092
093    /** The index position of this content value in the XML order. */
094    private int m_xmlIndex;
095
096    /**
097     * Default constructor for a XML content type
098     * that initializes some internal values.<p>
099     */
100    protected A_CmsXmlContentValue() {
101
102        m_minOccurs = 0;
103        m_maxOccurs = Integer.MAX_VALUE;
104        m_index = -1;
105        m_xmlIndex = -1;
106        m_maxIndex = -1;
107    }
108
109    /**
110     * Initializes the required members for this XML content value.<p>
111     *
112     * @param document the XML content instance this value belongs to
113     * @param element the XML element that contains this value
114     * @param locale the locale this value is created for
115     * @param type the type instance to create the value for
116     */
117    protected A_CmsXmlContentValue(I_CmsXmlDocument document, Element element, Locale locale, I_CmsXmlSchemaType type) {
118
119        m_element = element;
120        m_name = element.getName();
121        m_document = document;
122        m_locale = locale;
123        m_minOccurs = type.getMinOccurs();
124        m_maxOccurs = type.getMaxOccurs();
125        m_contentDefinition = type.getContentDefinition();
126        m_index = -1;
127        m_xmlIndex = -1;
128        m_maxIndex = -1;
129    }
130
131    /**
132     * Initializes the schema type descriptor values for this type descriptor.<p>
133     *
134     * @param name the name of the XML node containing the value according to the XML schema
135     * @param minOccurs minimum number of occurrences of this type according to the XML schema
136     * @param maxOccurs maximum number of occurrences of this type according to the XML schema
137     */
138    protected A_CmsXmlContentValue(String name, String minOccurs, String maxOccurs) {
139
140        m_name = name;
141        m_minOccurs = 1;
142        if (CmsStringUtil.isNotEmpty(minOccurs)) {
143            try {
144                m_minOccurs = Integer.parseInt(minOccurs);
145            } catch (NumberFormatException e) {
146                // ignore
147            }
148        }
149        m_maxOccurs = 1;
150        if (CmsStringUtil.isNotEmpty(maxOccurs)) {
151            if (CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_UNBOUNDED.equals(maxOccurs)) {
152                m_maxOccurs = Integer.MAX_VALUE;
153            } else {
154                try {
155                    m_maxOccurs = Integer.parseInt(maxOccurs);
156                } catch (NumberFormatException e) {
157                    // ignore
158                }
159            }
160        }
161        m_index = -1;
162        m_xmlIndex = -1;
163        m_maxIndex = -1;
164    }
165
166    /**
167     * Appends an element XML representation of this type to the given root node.<p>
168     *
169     * @param root the element to append the XML to
170     */
171    public void appendXmlSchema(Element root) {
172
173        Element element = root.addElement(CmsXmlContentDefinition.XSD_NODE_ELEMENT);
174        element.addAttribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_NAME, getName());
175        element.addAttribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_TYPE, getTypeName());
176        if ((getMinOccurs() > 1) || (getMinOccurs() == 0)) {
177            element.addAttribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_MIN_OCCURS, String.valueOf(getMinOccurs()));
178        }
179        if (getMaxOccurs() > 1) {
180            if (getMaxOccurs() == Integer.MAX_VALUE) {
181                element.addAttribute(
182                    CmsXmlContentDefinition.XSD_ATTRIBUTE_MAX_OCCURS,
183                    CmsXmlContentDefinition.XSD_ATTRIBUTE_VALUE_UNBOUNDED);
184            } else {
185                element.addAttribute(CmsXmlContentDefinition.XSD_ATTRIBUTE_MAX_OCCURS, String.valueOf(getMaxOccurs()));
186            }
187        }
188    }
189
190    /**
191     * @see java.lang.Comparable#compareTo(java.lang.Object)
192     */
193    public int compareTo(I_CmsXmlSchemaType obj) {
194
195        if (obj == this) {
196            return 0;
197        }
198        return getTypeName().compareTo(obj.getTypeName());
199    }
200
201    /**
202     * @see java.lang.Object#equals(java.lang.Object)
203     */
204    @Override
205    public boolean equals(Object obj) {
206
207        if (obj == this) {
208            return true;
209        }
210        if (obj instanceof I_CmsXmlSchemaType) {
211            I_CmsXmlSchemaType other = (I_CmsXmlSchemaType)obj;
212            return (getName().equals(other.getName())
213                && getTypeName().equals(other.getTypeName())
214                && (getMinOccurs() == other.getMinOccurs())
215                && (getMaxOccurs() == other.getMaxOccurs()));
216        }
217        return false;
218    }
219
220    /**
221     * @see org.opencms.xml.types.I_CmsXmlSchemaType#generateXml(org.opencms.file.CmsObject, org.opencms.xml.I_CmsXmlDocument, org.dom4j.Element, java.util.Locale)
222     */
223    public Element generateXml(CmsObject cms, I_CmsXmlDocument document, Element root, Locale locale) {
224
225        Element element = root.addElement(getName());
226        // get the default value from the content handler
227        String defaultValue = document.getHandler().getDefault(cms, this, locale);
228        if (defaultValue != null) {
229            try {
230                I_CmsXmlContentValue value = createValue(document, element, locale);
231                value.setStringValue(cms, defaultValue);
232            } catch (CmsRuntimeException e) {
233                // should not happen if default value is correct
234                LOG.error(
235                    Messages.get().getBundle().key(Messages.ERR_XMLCONTENT_INVALID_ELEM_DEFAULT_1, defaultValue),
236                    e);
237                element.clearContent();
238            }
239        }
240        return element;
241    }
242
243    /**
244     * @see org.opencms.xml.types.I_CmsXmlSchemaType#getChoiceMaxOccurs()
245     */
246    public int getChoiceMaxOccurs() {
247
248        return 0;
249    }
250
251    /**
252     * @see org.opencms.xml.types.I_CmsXmlSchemaType#getContentDefinition()
253     */
254    public CmsXmlContentDefinition getContentDefinition() {
255
256        return m_contentDefinition;
257    }
258
259    /**
260     * @see org.opencms.widgets.I_CmsWidgetParameter#getDefault(org.opencms.file.CmsObject)
261     */
262    public String getDefault(CmsObject cms) {
263
264        return m_contentDefinition.getContentHandler().getDefault(cms, this, getLocale());
265    }
266
267    /**
268     * @see org.opencms.xml.types.I_CmsXmlSchemaType#getDefault(java.util.Locale)
269     */
270    public String getDefault(Locale locale) {
271
272        return m_defaultValue;
273    }
274
275    /**
276     * @see org.opencms.xml.types.I_CmsXmlContentValue#getDocument()
277     */
278    public I_CmsXmlDocument getDocument() {
279
280        return m_document;
281    }
282
283    /**
284     * @see org.opencms.xml.types.I_CmsXmlContentValue#getElement()
285     */
286    public Element getElement() {
287
288        return m_element;
289    }
290
291    /**
292     * @see org.opencms.widgets.I_CmsWidgetParameter#getId()
293     */
294    public String getId() {
295
296        if (m_element == null) {
297            return null;
298        }
299
300        StringBuffer result = new StringBuffer(128);
301        result.append(getTypeName());
302        result.append('.');
303        // the '[', ']' and '/' chars from the xpath are invalid for html id's
304        result.append(getPath().replace('[', '_').replace(']', '_').replace('/', '.'));
305        result.append('.');
306        result.append(getIndex());
307        return result.toString();
308    }
309
310    /**
311     * @see org.opencms.xml.types.I_CmsXmlContentValue#getIndex()
312     */
313    public int getIndex() {
314
315        if (m_index < 0) {
316            if (isChoiceOption()) {
317                m_index = m_element.getParent().elements().indexOf(m_element);
318            } else {
319                m_index = getXmlIndex();
320            }
321        }
322        return m_index;
323    }
324
325    /**
326     * @see org.opencms.widgets.I_CmsWidgetParameter#getKey()
327     */
328    public String getKey() {
329
330        StringBuffer result = new StringBuffer(128);
331        if (CmsStringUtil.isNotEmptyOrWhitespaceOnly(m_prefix)) {
332            result.append(m_prefix);
333            result.append('.');
334        }
335        result.append(m_contentDefinition.getInnerName());
336        result.append('.');
337        result.append(getName());
338        return result.toString();
339    }
340
341    /**
342     * @see org.opencms.xml.types.I_CmsXmlContentValue#getLocale()
343     */
344    public Locale getLocale() {
345
346        return m_locale;
347    }
348
349    /**
350     * @see org.opencms.xml.types.I_CmsXmlContentValue#getMaxIndex()
351     */
352    public int getMaxIndex() {
353
354        if (m_maxIndex < 0) {
355            if (isChoiceOption()) {
356                m_maxIndex = m_element.getParent().elements().size();
357            } else {
358                m_maxIndex = m_element.getParent().elements(m_element.getQName()).size();
359            }
360        }
361        return m_maxIndex;
362    }
363
364    /**
365     * Returns the maximum occurrences of this type.<p>
366     *
367     * @return the maximum occurrences of this type
368     */
369    public int getMaxOccurs() {
370
371        return m_maxOccurs;
372    }
373
374    /**
375     * Returns the minimum occurrences of this type.<p>
376     *
377     * @return the minimum occurrences of this type
378     */
379    public int getMinOccurs() {
380
381        return m_minOccurs;
382    }
383
384    /**
385     * Returns the name.<p>
386     *
387     * @return the name
388     */
389    public String getName() {
390
391        return m_name;
392    }
393
394    /**
395     * @see org.opencms.xml.types.I_CmsXmlContentValue#getPath()
396     */
397    public String getPath() {
398
399        if (m_element == null) {
400            return "";
401        }
402        String path = m_element.getUniquePath();
403        // must remove the first 2 nodes because these are not required for XML content values
404        int pos = path.indexOf('/', path.indexOf('/', 1) + 1) + 1;
405        path = path.substring(pos);
406
407        // ensure all path elements have an index, even though this may not be required
408        return CmsXmlUtils.createXpath(path, 1);
409    }
410
411    /**
412     * @see org.opencms.xml.types.I_CmsXmlContentValue#getPlainText(org.opencms.file.CmsObject)
413     */
414    public String getPlainText(CmsObject cms) {
415
416        return null;
417    }
418
419    /**
420     * @see org.opencms.xml.types.I_CmsXmlContentValue#getXmlIndex()
421     */
422    public int getXmlIndex() {
423
424        if (m_xmlIndex < 0) {
425            m_xmlIndex = m_element.getParent().elements(m_element.getQName()).indexOf(m_element);
426        }
427        return m_xmlIndex;
428    }
429
430    /**
431     * @see org.opencms.widgets.I_CmsWidgetParameter#hasError()
432     */
433    public boolean hasError() {
434
435        return false;
436    }
437
438    /**
439     * @see java.lang.Object#hashCode()
440     */
441    @Override
442    public int hashCode() {
443
444        return getTypeName().hashCode();
445    }
446
447    /**
448     * @see org.opencms.xml.types.I_CmsXmlSchemaType#isChoiceOption()
449     */
450    public boolean isChoiceOption() {
451
452        return m_contentDefinition.getChoiceMaxOccurs() > 0;
453    }
454
455    /**
456     * @see org.opencms.xml.types.I_CmsXmlSchemaType#isChoiceType()
457     */
458    public boolean isChoiceType() {
459
460        // this is overridden in the nested content definition
461        return false;
462    }
463
464    /**
465     * The default implementation always returns <code>true</code>.<p>
466     *
467     * @see org.opencms.xml.types.I_CmsXmlContentValue#isSearchable()
468     */
469    public boolean isSearchable() {
470
471        return true;
472    }
473
474    /**
475     * @see org.opencms.xml.types.I_CmsXmlSchemaType#isSimpleType()
476     */
477    public boolean isSimpleType() {
478
479        // the abstract base type should be used for simple types only
480        return true;
481    }
482
483    /**
484     * @see org.opencms.xml.types.I_CmsXmlContentValue#moveDown()
485     */
486    public void moveDown() {
487
488        if (getIndex() > 0) {
489            // only move down if this element is not already at the first index position
490            moveValue(-1);
491            getDocument().initDocument();
492        }
493    }
494
495    /**
496     * @see org.opencms.xml.types.I_CmsXmlContentValue#moveUp()
497     */
498    public void moveUp() {
499
500        if (getIndex() < (getMaxIndex() - 1)) {
501            // only move up if this element is not already at the last index position
502            moveValue(1);
503            getDocument().initDocument();
504        }
505    }
506
507    /**
508     * @see org.opencms.xml.types.I_CmsXmlSchemaType#setContentDefinition(org.opencms.xml.CmsXmlContentDefinition)
509     */
510    public void setContentDefinition(CmsXmlContentDefinition contentDefinition) {
511
512        m_contentDefinition = contentDefinition;
513    }
514
515    /**
516     * Sets the default value for a node of this type.<p>
517     *
518     * @param defaultValue the default value to set
519     */
520    public void setDefault(String defaultValue) {
521
522        m_defaultValue = defaultValue;
523    }
524
525    /**
526     * @see org.opencms.widgets.I_CmsWidgetParameter#setKeyPrefix(java.lang.String)
527     */
528    public void setKeyPrefix(String prefix) {
529
530        m_prefix = prefix;
531    }
532
533    /**
534     * @see java.lang.Object#toString()
535     */
536    @Override
537    public String toString() {
538
539        StringBuffer result = new StringBuffer(128);
540        result.append(getClass().getName());
541        result.append(": name=");
542        result.append(getName());
543        result.append(", type=");
544        result.append(getTypeName());
545        result.append(", path=");
546        result.append(m_element == null ? null : getPath());
547        String value;
548        try {
549            value = "'" + getStringValue(null) + "'";
550        } catch (Exception e) {
551            value = "(CmsObject required to generate)";
552        }
553        result.append(", value=");
554        result.append(value);
555        return result.toString();
556    }
557
558    /**
559     * @see org.opencms.xml.types.I_CmsXmlSchemaType#validateValue(java.lang.String)
560     */
561    public boolean validateValue(String value) {
562
563        return true;
564    }
565
566    /**
567     * Returns the relation type for the given path.<p>
568     *
569     * @param path the element path
570     *
571     * @return the relation type
572     */
573    protected CmsRelationType getRelationType(String path) {
574
575        CmsRelationType result = getContentDefinition().getContentHandler().getRelationType(path);
576        I_CmsXmlDocument document = getDocument();
577        if (document != null) {
578            // the relations set in the main content definition override relations set in the nested definition
579            result = document.getContentDefinition().getContentHandler().getRelationType(getPath(), result);
580        } else {
581            LOG.warn("Missing document while evaluating relation type for " + path);
582        }
583        return result;
584    }
585
586    /**
587     * Moves this XML content element up or down in the XML document.<p>
588     *
589     * Please note: No check is performed if the move violates the XML document schema!<p>
590     *
591     * @param step move the element up or down according to the given step value
592     */
593    protected void moveValue(int step) {
594
595        Element e = getElement();
596        Element parent = e.getParent();
597        List<Element> siblings = CmsXmlGenericWrapper.elements(parent);
598        int idx = siblings.indexOf(e);
599        int newIdx = idx + step;
600        siblings.remove(idx);
601        siblings.add(newIdx, e);
602        m_index += step;
603    }
604
605    /**
606     * Convenience method to loads the XML schema definition for this value type from an external file.<p>
607     *
608     * @param schemaUri the schema uri to load the XML schema file from
609     *
610     * @return the loaded XML schema
611     *
612     * @throws CmsRuntimeException if something goes wrong
613     */
614    protected String readSchemaDefinition(String schemaUri) throws CmsRuntimeException {
615
616        // the schema definition is located in a separate file for easier editing
617        String schemaDefinition;
618        try {
619            schemaDefinition = CmsFileUtil.readFile(schemaUri, CmsEncoder.ENCODING_UTF_8);
620        } catch (Exception e) {
621            throw new CmsRuntimeException(
622                Messages.get().container(Messages.ERR_XMLCONTENT_LOAD_SCHEMA_1, schemaUri),
623                e);
624        }
625        return schemaDefinition;
626    }
627}