001/*
002 *  Copyright 2018 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.cms.contenttype;
017
018import java.io.File;
019import java.io.IOException;
020import java.io.OutputStream;
021import java.net.MalformedURLException;
022import java.nio.file.Files;
023import java.nio.file.Paths;
024import java.util.List;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.Properties;
028import java.util.Set;
029
030import javax.xml.transform.OutputKeys;
031import javax.xml.transform.TransformerConfigurationException;
032import javax.xml.transform.TransformerFactory;
033import javax.xml.transform.TransformerFactoryConfigurationError;
034import javax.xml.transform.sax.SAXTransformerFactory;
035import javax.xml.transform.sax.TransformerHandler;
036import javax.xml.transform.stream.StreamResult;
037
038import org.apache.avalon.framework.component.Component;
039import org.apache.avalon.framework.configuration.ConfigurationException;
040import org.apache.avalon.framework.configuration.DefaultConfigurationSerializer;
041import org.apache.avalon.framework.service.ServiceException;
042import org.apache.avalon.framework.service.ServiceManager;
043import org.apache.avalon.framework.service.Serviceable;
044import org.apache.cocoon.xml.AttributesImpl;
045import org.apache.cocoon.xml.XMLUtils;
046import org.apache.commons.io.FileUtils;
047import org.apache.commons.lang.StringUtils;
048import org.apache.excalibur.source.SourceResolver;
049import org.apache.excalibur.source.impl.FileSource;
050import org.apache.xml.serializer.OutputPropertiesFactory;
051import org.xml.sax.SAXException;
052
053import org.ametys.cms.repository.Content;
054import org.ametys.cms.repository.ContentDAO;
055import org.ametys.cms.repository.ContentQueryHelper;
056import org.ametys.cms.repository.ContentTypeOrMixinExpression;
057import org.ametys.plugins.repository.AmetysObjectIterable;
058import org.ametys.plugins.repository.AmetysObjectResolver;
059import org.ametys.plugins.repository.model.CompositeDefinition;
060import org.ametys.plugins.repository.model.RepeaterDefinition;
061import org.ametys.plugins.repository.query.expression.Expression;
062import org.ametys.plugins.repository.query.expression.Expression.Operator;
063import org.ametys.runtime.i18n.I18nizableText;
064import org.ametys.runtime.model.ElementDefinition;
065import org.ametys.runtime.model.Enumerator;
066import org.ametys.runtime.model.ModelItem;
067import org.ametys.runtime.model.ModelItemContainer;
068import org.ametys.runtime.model.ModelViewItem;
069import org.ametys.runtime.model.SimpleViewItemGroup;
070import org.ametys.runtime.model.StaticEnumerator;
071import org.ametys.runtime.model.View;
072import org.ametys.runtime.model.ViewItem;
073import org.ametys.runtime.model.ViewItemContainer;
074import org.ametys.runtime.parameter.DefaultValidator;
075import org.ametys.runtime.parameter.Validator;
076import org.ametys.runtime.plugin.component.AbstractLogEnabled;
077
078/**
079 * Helper for create, edit and remove a content type
080 */
081public class EditContentTypeHelper extends AbstractLogEnabled implements Component, Serviceable
082{
083    /** The Avalon role name */
084    public static final String ROLE = EditContentTypeHelper.class.getName();
085
086    /** The directory path of content types */
087    private static final String __CONTENT_TYPES_DIR = "context://WEB-INF/param/content-types/";
088    /** The prefix for id of automatic content types */
089    private static final String __CONTENT_TYPE_PREFIX = "content-type.";
090
091    /** The namespace uri */
092    private static final String __NAMESPACE_URI = "http://www.ametys.org/schema/cms";
093
094    /** The source resolver */
095    protected SourceResolver _sourceResolver;
096
097    /** The ametys object resolver instance */
098    protected AmetysObjectResolver _ametysObjectResolver;
099
100    /** The content type extension point instance */
101    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
102
103    /** DAO for contents */
104    protected ContentDAO _contentDAO;
105
106    public void service(ServiceManager manager) throws ServiceException
107    {
108        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
109        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
110        _ametysObjectResolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
111        _contentDAO = (ContentDAO) manager.lookup(ContentDAO.ROLE);
112    }
113
114    /**
115     * Get the root directory for all custom content types. The directory is created if does not exist.
116     * @return the root directory for all custom content types.
117     * @throws MalformedURLException if failed to get the root directory
118     * @throws IOException if failed to get the root directory
119     */
120    public File getContentTypeRootDirectory() throws MalformedURLException, IOException
121    {
122        FileSource rootSource = null;
123        try
124        {
125            rootSource = (FileSource) _sourceResolver.resolveURI(__CONTENT_TYPES_DIR);
126            File rootDir = rootSource.getFile();
127            if (!rootDir.exists())
128            {
129                rootDir.mkdir();
130            }
131            return rootDir;
132        }
133        finally
134        {
135            _sourceResolver.release(rootSource);
136        }
137    }
138    
139    /**
140     * Get the root directory for all archived content types. The directory is created if does not exist.
141     * @return the root directory for archived content types.
142     * @throws MalformedURLException if failed to get the root directory
143     * @throws IOException if failed to get the root directory
144     */
145    public File getArchiveRootDirectory() throws MalformedURLException, IOException
146    {
147        File rootDir = getContentTypeRootDirectory();
148        
149        File archiveDir = new File(rootDir, "_archives");
150        if (!archiveDir.exists())
151        {
152            archiveDir.mkdir();
153        }
154        
155        return archiveDir;
156    }
157    
158    /**
159     * Get the root archive directory for a given content type. The directory is created if does not exist.
160     * @param contentType The content type
161     * @return the root archive directory for this content type.
162     * @throws MalformedURLException if failed to get the archive directory
163     * @throws IOException if failed to get the archive directory
164     */
165    public File getArchiveRootDirectory(ContentType contentType) throws MalformedURLException, IOException
166    {
167        File archiveDir = getArchiveRootDirectory();
168        
169        File cTypeDir = new File(archiveDir, contentType.getPluginName());
170        if (!cTypeDir.exists())
171        {
172            cTypeDir.mkdir();
173        }
174        
175        return cTypeDir;
176    }
177    
178    /**
179     * Get the root directory for a given content type. The directory is created if does not exist.
180     * @param contentType The content type definition
181     * @param override true if this is a overriden content type.
182     * @return the root directory for this content type.
183     * @throws MalformedURLException if failed to get the archive directory
184     * @throws IOException if failed to get the archive directory
185     */
186    public File getContentTypeDirectory(ContentTypeDefinition contentType, boolean override) throws MalformedURLException, IOException
187    {
188        File rootDir = getContentTypeRootDirectory();
189        
190        File cTypeDir = new File(rootDir, override ? "_override" : contentType.getPluginName());
191        if (!cTypeDir.exists())
192        {
193            cTypeDir.mkdir();
194        }
195        
196        return cTypeDir;
197    }
198    
199    /**
200     * Get the root directory for a given content type. The directory is created if does not exist.
201     * @param contentType The content type
202     * @return the root directory for this content type.
203     * @throws MalformedURLException if failed to get the archive directory
204     * @throws IOException if failed to get the archive directory
205     */
206    public File getContentTypeDirectory(ContentType contentType) throws MalformedURLException, IOException
207    {
208        File rootDir = getContentTypeRootDirectory();
209        
210        File cTypeDir = new File(rootDir, contentType.getPluginName());
211        if (!cTypeDir.exists())
212        {
213            cTypeDir.mkdir();
214        }
215        
216        return cTypeDir;
217    }
218    
219    /**
220     * Get the XML definition file for a given content type.
221     * @param contentType The content type definition
222     * @param override true if this is a overriden content type.
223     * @return the XML definition file for this content type.
224     * @throws MalformedURLException if failed to get the archive directory
225     * @throws IOException if failed to get the archive directory
226     */
227    public File getContentTypeFile(ContentTypeDefinition contentType, boolean override) throws MalformedURLException, IOException
228    {
229        File rootCTypeDir = getContentTypeDirectory(contentType, override);
230        
231        String id = contentType.getId().replaceFirst(__CONTENT_TYPE_PREFIX, "");
232        return new File(rootCTypeDir, id + ".xml");
233    }
234    
235    /**
236     * Get the XML definition file for a given content type.
237     * @param contentType The content type
238     * @return the XML definition file for this content type.
239     * @throws MalformedURLException if failed to get the archive directory
240     * @throws IOException if failed to get the archive directory
241     */
242    public File getContentTypeFile(ContentType contentType) throws MalformedURLException, IOException
243    {
244        File rootCTypeDir = getContentTypeDirectory(contentType);
245        
246        String id = contentType.getId().replaceFirst(__CONTENT_TYPE_PREFIX, "");
247        return new File(rootCTypeDir, id + ".xml");
248    }
249    
250    /**
251     * Create a new content type. 
252     * This method creates a new xml file into WEB-INF/param/content-types folder.
253     * @param contentTypeDef The content type definition
254     * @throws EditContentTypeException if failed to create the content type
255     */
256    public void createContentType(ContentTypeDefinition contentTypeDef) throws EditContentTypeException
257    {
258        String id = contentTypeDef.getId().replaceFirst(__CONTENT_TYPE_PREFIX, "");
259        String realId = __CONTENT_TYPE_PREFIX + id;
260
261        Set<String> extensionsIds = _contentTypeExtensionPoint.getExtensionsIds();
262        if (!extensionsIds.contains(realId))
263        {
264            if (StringUtils.isNotBlank(contentTypeDef.getPluginName()))
265            {
266                _saxContentType(contentTypeDef, false);
267            }
268            else
269            {
270                throw new EditContentTypeException("The content type '" + contentTypeDef.getId() + "' can't be created without plugin name.");
271            }
272        }
273        else
274        {
275            throw new EditContentTypeException("The content type '" + contentTypeDef.getId() + "' can't be created because it already exists.");
276        }
277    }
278
279    /**
280     * Modify a existing content type. 
281     * This method edit the corresponding XML file of the content type in WEB-INF/param/content-types folder.
282     * @param contentTypeDef The content type definition
283     * @throws EditContentTypeException if failed to edit this content type
284     */
285    public void editContentType(ContentTypeDefinition contentTypeDef) throws EditContentTypeException
286    {
287        // For the content type id we consider that it is the real id, for
288        // example for content type in web-inf the id is prefixed by
289        // "content-type."
290        Set<String> extensionsIds = _contentTypeExtensionPoint.getExtensionsIds();
291        if (extensionsIds.contains(contentTypeDef.getId()))
292        {
293            boolean override = !_isAutomaticContentType(contentTypeDef.getId());
294            _saxContentType(contentTypeDef, override);
295        }
296        else
297        {
298            throw new EditContentTypeException("The content type '" + contentTypeDef.getId() + "' can't be edited because this content type doesn't exist.");
299        }
300    }
301
302    private void _saxContentType(ContentTypeDefinition contentTypeDef, boolean override) throws EditContentTypeException
303    {
304        try 
305        {
306            File cTypeFile = getContentTypeFile(contentTypeDef, override);
307
308            // Sax the content type in file
309            try (OutputStream os = Files.newOutputStream(Paths.get(cTypeFile.getAbsolutePath())))
310            {
311                // Create a transformer for saving sax into a file
312                TransformerHandler handler = ((SAXTransformerFactory) TransformerFactory.newInstance()).newTransformerHandler();
313
314                StreamResult result = new StreamResult(os);
315                handler.setResult(result);
316
317                // Create the format of result
318                Properties format = new Properties();
319                format.put(OutputKeys.METHOD, "xml");
320                format.put(OutputKeys.INDENT, "yes");
321                format.put(OutputKeys.ENCODING, "UTF-8");
322                format.put(OutputPropertiesFactory.S_KEY_INDENT_AMOUNT, "4");
323                handler.getTransformer().setOutputProperties(format);
324
325                try
326                {
327                    _saxContentType(contentTypeDef, override, handler);
328                }
329                catch (SAXException | ConfigurationException e)
330                {
331                    throw new EditContentTypeException("Error when trying to sax the content type '" + contentTypeDef.getId() + "'", e);
332                }
333            }
334            catch (IOException | TransformerConfigurationException | TransformerFactoryConfigurationError e)
335            {
336                throw new EditContentTypeException(e);
337            }
338        }
339        catch (IOException e)
340        {
341            throw new EditContentTypeException(e.getMessage(), e);
342        }
343    }
344
345    private void _saxContentType(ContentTypeDefinition contentTypeDef, boolean override, TransformerHandler handler)
346            throws SAXException, EditContentTypeException, ConfigurationException
347    {
348        handler.startDocument();
349        handler.startPrefixMapping("cms", __NAMESPACE_URI);
350
351        if (override)
352        {
353            XMLUtils.startElement(handler, "cms:content-type");
354        }
355        else
356        {
357            AttributesImpl attributes = new AttributesImpl();
358
359            if (contentTypeDef.isAbstract())
360            {
361                attributes.addCDATAAttribute("abstract", "true");
362            }
363            if (contentTypeDef.getSupertypeIds() != null && contentTypeDef.getSupertypeIds().length != 0)
364            {
365                attributes.addCDATAAttribute("extends", String.join(",", contentTypeDef.getSupertypeIds()));
366            }
367            XMLUtils.startElement(handler, "cms:content-type", attributes);
368
369            _saxGeneralInformation(contentTypeDef, handler);
370            _saxIcons(contentTypeDef.getIconGlyph(), handler);
371            _saxTags(contentTypeDef.getTags(), handler);
372            _saxParentRef(contentTypeDef.getParentRef(), handler);
373            _saxRight(contentTypeDef.getRight(), handler);
374        }
375
376        _saxModelItems(contentTypeDef, handler);
377        _saxViews(contentTypeDef, handler);
378
379        XMLUtils.endElement(handler, "cms:content-type");
380        handler.endDocument();
381    }
382
383    private void _saxGeneralInformation(ContentTypeDefinition contentTypeDef, TransformerHandler handler) throws SAXException, EditContentTypeException
384    {
385        I18nizableText label = contentTypeDef.getLabel();
386        if (label == null)
387        {
388            throw new EditContentTypeException("The content type '" + contentTypeDef.getId() + "' can't be created without label");
389        }
390        else
391        {
392            _saxI18nizableText(handler, "cms:label", label);
393        }
394
395        _saxI18nizableText(handler, "cms:description", contentTypeDef.getDescription());
396        
397        I18nizableText defaultTitle = contentTypeDef.getDefaultTitle();
398        if (defaultTitle == null)
399        {
400            throw new EditContentTypeException("The content type '" + contentTypeDef.getId() + "' can't be created without default title");
401        }
402        else
403        {
404            _saxI18nizableText(handler, "cms:default-title", defaultTitle);
405        }
406        
407        _saxI18nizableText(handler, "cms:category", contentTypeDef.getCategory());
408    }
409
410    private void _saxIcons(String iconGlyph, TransformerHandler handler) throws SAXException
411    {
412        if (StringUtils.isNotBlank(iconGlyph))
413        {
414            XMLUtils.startElement(handler, "cms:icons");
415            XMLUtils.createElement(handler, "cms:glyph", iconGlyph);
416            XMLUtils.endElement(handler, "cms:icons");
417        }
418    }
419
420    private void _saxTags(Set<String> tags, TransformerHandler handler) throws SAXException
421    {
422        if (tags != null && !tags.isEmpty())
423        {
424            XMLUtils.startElement(handler, "cms:tags");
425
426            for (String tag : tags)
427            {
428                XMLUtils.createElement(handler, "cms:tag", tag);
429            }
430
431            XMLUtils.endElement(handler, "cms:tags");
432        }
433    }
434
435    private void _saxParentRef(String parentRef, TransformerHandler handler) throws SAXException
436    {
437        if (StringUtils.isNotBlank(parentRef))
438        {
439            AttributesImpl attributes = new AttributesImpl();
440            attributes.addCDATAAttribute("name", parentRef);
441            XMLUtils.createElement(handler, "cms:parent-ref", attributes);
442        }
443    }
444
445    private void _saxRight(String right, TransformerHandler handler) throws SAXException
446    {
447        if (StringUtils.isNotBlank(right))
448        {
449            XMLUtils.createElement(handler, "cms:right", right);
450        }
451    }
452
453    private void _saxModelItems(ContentTypeDefinition contentTypeDef, TransformerHandler handler) throws SAXException, ConfigurationException
454    {
455        if (contentTypeDef.getModelItems() != null)
456        {
457            for (ModelItem modelItem : contentTypeDef.getModelItems())
458            {
459                _saxModelItem(modelItem, handler);
460            }
461        }
462    }
463
464    @SuppressWarnings("unchecked")
465    private void _saxModelItem(ModelItem modelItem, TransformerHandler handler) throws SAXException, ConfigurationException
466    {
467        if (modelItem != null)
468        {
469            if (modelItem.getType() != null)
470            {
471                if (modelItem instanceof RepeaterDefinition)
472                {
473                    _saxRepeater((RepeaterDefinition) modelItem, handler);
474                }
475                else if (modelItem instanceof CompositeDefinition)
476                {
477                    if ("dc".equals(modelItem.getName())) // Dublin core exception
478                    {
479                        XMLUtils.createElement(handler, "cms:dublin-core");
480                    }
481                    else
482                    {
483                        _saxComposite((CompositeDefinition) modelItem, handler);
484                    }
485                }
486                else if (modelItem instanceof ElementDefinition)
487                {
488                    _saxElementDefinition((ElementDefinition) modelItem, handler);
489                }
490            }
491        }
492    }
493
494    private void _saxRepeater(RepeaterDefinition repeater, TransformerHandler handler) throws SAXException, ConfigurationException
495    {
496        AttributesImpl attributes = new AttributesImpl();
497        attributes.addCDATAAttribute("name", repeater.getName());
498
499        String initialSize = Integer.toString(repeater.getInitialSize());
500        String minSize = Integer.toString(repeater.getMinSize());
501        String maxSize = Integer.toString(repeater.getMaxSize());
502        if (StringUtils.isNotBlank(initialSize))
503        {
504            attributes.addCDATAAttribute("initial-size", initialSize);
505        }
506        if (StringUtils.isNotBlank(minSize))
507        {
508            attributes.addCDATAAttribute("min-size", minSize);
509        }
510        if (StringUtils.isNotBlank(maxSize))
511        {
512            attributes.addCDATAAttribute("max-size", maxSize);
513        }
514
515        XMLUtils.startElement(handler, "cms:repeater", attributes);
516
517        _saxI18nizableText(handler, "cms:label", repeater.getLabel());
518        _saxI18nizableText(handler, "cms:description", repeater.getDescription());
519        _saxI18nizableText(handler, "cms:add-label", repeater.getAddLabel());
520        _saxI18nizableText(handler, "cms:del-label", repeater.getDeleteLabel());
521        _saxChildrenModelItems(repeater, handler);
522
523        XMLUtils.endElement(handler, "cms:repeater");
524    }
525
526    private void _saxComposite(CompositeDefinition composite, TransformerHandler handler) throws SAXException, ConfigurationException
527    {
528        AttributesImpl attributes = new AttributesImpl();
529        attributes.addCDATAAttribute("name", composite.getName());
530        attributes.addCDATAAttribute("type", composite.getType().getId());
531
532        XMLUtils.startElement(handler, "cms:metadata", attributes);
533
534        _saxI18nizableText(handler, "cms:label", composite.getLabel());
535        _saxI18nizableText(handler, "cms:description", composite.getDescription());
536        _saxChildrenModelItems(composite, handler);
537
538        XMLUtils.endElement(handler, "cms:metadata");
539    }
540
541    private void _saxChildrenModelItems(ModelItemContainer modelItemContainer, TransformerHandler handler) throws ConfigurationException, SAXException
542    {
543        if (modelItemContainer.getModelItems() != null)
544        {
545            for (ModelItem modelItem : modelItemContainer.getModelItems())
546            {
547                _saxModelItem(modelItem, handler);
548            }
549        }
550    }
551    
552    private void _saxElementDefinition(ElementDefinition<? extends Object> elementDefinition, TransformerHandler handler) throws SAXException, ConfigurationException
553    {
554        AttributesImpl attributes = new AttributesImpl();
555        attributes.addCDATAAttribute("name", elementDefinition.getName());
556        attributes.addCDATAAttribute("multiple", Boolean.toString(elementDefinition.isMultiple()));
557        attributes.addCDATAAttribute("type", elementDefinition.getType().getId());
558
559        if (elementDefinition instanceof ContentAttributeDefinition)
560        {
561            ContentAttributeDefinition contentAttributeDefinition = (ContentAttributeDefinition) elementDefinition;
562            String linkedContentTypeId = contentAttributeDefinition.getContentTypeId();
563            if (linkedContentTypeId != null)
564            {
565                attributes.addCDATAAttribute("contentType", linkedContentTypeId);
566                if (contentAttributeDefinition.getInvertRelationPath() != null)
567                {
568                    attributes.addCDATAAttribute("invert", contentAttributeDefinition.getInvertRelationPath());
569                }
570            }
571        }
572        
573        XMLUtils.startElement(handler, "cms:metadata", attributes);
574
575        _saxI18nizableText(handler, "cms:label", elementDefinition.getLabel());
576        _saxI18nizableText(handler, "cms:description", elementDefinition.getDescription());
577        _saxWidget(elementDefinition, handler);
578        _saxEnumeration(elementDefinition, handler);
579        _saxValidator(elementDefinition, handler);
580
581        XMLUtils.endElement(handler, "cms:metadata");
582    }
583
584    private void _saxWidget(ElementDefinition<? extends Object> elementDefinition, TransformerHandler handler) throws SAXException
585    {
586        if (StringUtils.isNotBlank(elementDefinition.getWidget()))
587        {
588            XMLUtils.createElement(handler, "widget", elementDefinition.getWidget());
589        }
590
591        if (elementDefinition.getWidgetParameters() != null)
592        {
593            XMLUtils.startElement(handler, "widget-params");
594
595            for (Entry<String, I18nizableText> widgetParam : elementDefinition.getWidgetParameters().entrySet())
596            {
597                if (widgetParam.getValue() != null)
598                {
599                    AttributesImpl attributes = new AttributesImpl();
600                    attributes.addCDATAAttribute("name", widgetParam.getKey());
601                    _saxI18nizableText(handler, "param", widgetParam.getValue(), attributes);
602                }
603            }
604
605            XMLUtils.endElement(handler, "widget-params");
606        }
607    }
608
609    private void _saxEnumeration(ElementDefinition elementDefinition, TransformerHandler handler) throws SAXException, ConfigurationException
610    {
611        Enumerator enumerator = elementDefinition.getEnumerator();
612        if (enumerator != null && enumerator instanceof StaticEnumerator)
613        {
614            _saxDefaultEnumerator(elementDefinition, handler);
615        }
616        else if (StringUtils.isNotBlank(elementDefinition.getCustomEnumerator()))
617        {
618            _saxCustomEnumerator(elementDefinition, handler);
619        }
620    }
621
622    private void _saxDefaultEnumerator(ElementDefinition elementDefinition, TransformerHandler handler)
623    {
624        try
625        {
626            Map<Object, I18nizableText> entries = elementDefinition.getEnumerator().getTypedEntries();
627
628            if (entries != null)
629            {
630                // Enumerated values
631                XMLUtils.startElement(handler, "enumeration");
632                for (Entry<Object, I18nizableText> entry : entries.entrySet())
633                {
634                    if (entry != null)
635                    {
636                        if (entry.getValue() != null)
637                        {
638                            XMLUtils.startElement(handler, "entry");
639                            XMLUtils.createElement(handler, "value", entry.getKey().toString());
640                            _saxI18nizableText(handler, "label", entry.getValue());
641                            XMLUtils.endElement(handler, "entry");
642                        }
643                    }
644                }
645                XMLUtils.endElement(handler, "enumeration");
646            }
647        }
648        catch (Exception e)
649        {
650            // Silently ignore
651        }
652    }
653
654    private void _saxCustomEnumerator(ElementDefinition elementDefinition, TransformerHandler handler) throws SAXException, ConfigurationException
655    {
656        XMLUtils.startElement(handler, "enumeration");
657
658        AttributesImpl attributes = new AttributesImpl();
659        attributes.addCDATAAttribute("class", elementDefinition.getCustomEnumerator());
660        XMLUtils.startElement(handler, "custom-enumerator", attributes);
661
662        if (elementDefinition.getEnumeratorConfiguration() != null)
663        {
664            DefaultConfigurationSerializer configurationSerializer = new DefaultConfigurationSerializer();
665            configurationSerializer.serialize(handler, elementDefinition.getEnumeratorConfiguration());
666        }
667
668        XMLUtils.endElement(handler, "custom-enumerator");
669
670        XMLUtils.endElement(handler, "enumeration");
671    }
672
673    private void _saxValidator(ElementDefinition elementDefinition, TransformerHandler handler) throws SAXException, ConfigurationException
674    {
675        Validator validator = elementDefinition.getValidator();
676        if (validator != null && validator.getClass().equals(DefaultValidator.class))
677        {
678            _saxDefaultValidator(elementDefinition, handler);
679        }
680        else if (StringUtils.isNotBlank(elementDefinition.getCustomValidator()))
681        {
682            _saxCustomValidator(elementDefinition, handler);
683        }
684    }
685
686    private void _saxDefaultValidator(ElementDefinition elementDefinition, TransformerHandler handler) throws SAXException
687    {
688        Validator validator = elementDefinition.getValidator();
689
690        XMLUtils.startElement(handler, "validation");
691
692        Map<String, Object> configuration = validator.getConfiguration();
693
694        // Mandatory traitment
695        Object mandatory = configuration.get("mandatory");
696        if (mandatory != null && mandatory.toString().equals("true"))
697        {
698            XMLUtils.createElement(handler, "mandatory");
699        }
700        // Regexp traitment
701        Object regexp = configuration.get("regexp");
702        if (regexp != null)
703        {
704            XMLUtils.createElement(handler, "regexp", regexp.toString());
705        }
706        // Invalid text traitment
707        Object invalidTextAsObject = configuration.get("invalidText");
708        if (invalidTextAsObject != null)
709        {
710            I18nizableText invalidTextAsI18nizableText = (I18nizableText) invalidTextAsObject;
711            _saxI18nizableText(handler, "invalidText", invalidTextAsI18nizableText);
712        }
713
714        XMLUtils.endElement(handler, "validation");
715    }
716
717    private void _saxCustomValidator(ElementDefinition elementDefinition, TransformerHandler handler) throws SAXException, ConfigurationException
718    {
719        XMLUtils.startElement(handler, "validation");
720
721        AttributesImpl attributes = new AttributesImpl();
722        attributes.addCDATAAttribute("class", elementDefinition.getCustomValidator());
723        XMLUtils.startElement(handler, "custom-validator", attributes);
724
725        if (elementDefinition.getValidatorConfiguration() != null)
726        {
727            DefaultConfigurationSerializer configurationSerializer = new DefaultConfigurationSerializer();
728            configurationSerializer.serialize(handler, elementDefinition.getValidatorConfiguration());
729        }
730
731        XMLUtils.endElement(handler, "custom-validator");
732
733        XMLUtils.endElement(handler, "validation");
734    }
735
736    private void _saxViews(ContentTypeDefinition contentType, TransformerHandler handler) throws SAXException
737    {
738        List<View> views = contentType.getViews();
739        if (views != null)
740        {
741            for (View view : views)
742            {
743                _saxView(view, handler);
744            }
745        }
746        else
747        {
748            _saxDefaultViews(handler);
749        }
750    }
751
752    private void _saxDefaultViews(TransformerHandler handler) throws SAXException
753    {
754        _saxDefaultView("main", "view", handler);
755        _saxDefaultView("main", "edition", handler);
756        _saxDefaultView("abstract", "view", handler);
757        _saxDefaultView("link", "view", handler);
758        _saxDefaultView("details", "view", handler);
759        _saxDefaultView("index", "view", handler);
760    }
761
762    private void _saxDefaultView(String name, String view, TransformerHandler handler) throws SAXException
763    {
764        AttributesImpl attributes = new AttributesImpl();
765        attributes.addCDATAAttribute("name", name);
766        attributes.addCDATAAttribute("type", view);
767        XMLUtils.createElement(handler, "cms:metadata-set", attributes);
768    }
769    
770    private void _saxView(View view, TransformerHandler handler) throws SAXException
771    {
772        AttributesImpl attributes = new AttributesImpl();
773        attributes.addCDATAAttribute("name", view.getName());
774        attributes.addCDATAAttribute("type", "view");
775        attributes.addCDATAAttribute("internal", Boolean.toString(view.isInternal()));
776        
777        XMLUtils.startElement(handler, "cms:metadata-set", attributes);
778        
779        _saxI18nizableText(handler, "cms:label", view.getLabel());
780        _saxI18nizableText(handler, "cms:description", view.getDescription());
781        for (ViewItem viewItem : view.getViewItems())
782        {
783            _saxViewItem(viewItem, handler);
784        }
785        
786        XMLUtils.endElement(handler, "cms:metadata-set");
787    }
788
789    private void _saxViewItem(ViewItem viewItem, TransformerHandler handler) throws SAXException
790    {
791        if (viewItem instanceof ModelViewItem)
792        {
793            ModelViewItem modelViewItem = (ModelViewItem) viewItem;
794            
795            if ("dc".equals(modelViewItem.getDefinition().getName())) // dublin core exception
796            {
797                XMLUtils.createElement(handler, "cms:dublin-core");
798            }
799            else
800            {
801                AttributesImpl attributes = new AttributesImpl();
802                attributes.addCDATAAttribute("name", modelViewItem.getDefinition().getName());
803                
804                XMLUtils.startElement(handler, "cms:metadata-ref", attributes);
805                if (modelViewItem instanceof ViewItemContainer)
806                {
807                    for (ViewItem child : ((ViewItemContainer) modelViewItem).getViewItems())
808                    {
809                        _saxViewItem(child, handler);
810                    }
811                }
812                XMLUtils.endElement(handler, "cms:metadata-ref");
813            }
814        }
815        else if (viewItem instanceof SimpleViewItemGroup)
816        {
817            SimpleViewItemGroup simpleViewItemGroup = (SimpleViewItemGroup) viewItem;
818            AttributesImpl attributes = new AttributesImpl();
819            attributes.addCDATAAttribute("role", simpleViewItemGroup.getRole());
820            
821            XMLUtils.startElement(handler, "cms:fieldset", attributes);
822            
823            _saxI18nizableText(handler, "cms:label", simpleViewItemGroup.getLabel());
824            for (ViewItem child : simpleViewItemGroup.getViewItems())
825            {
826                _saxViewItem(child, handler);
827            }
828            
829            XMLUtils.endElement(handler, "cms:fieldset");
830        }
831    }
832
833    private void _saxI18nizableText(TransformerHandler handler, String tagName, I18nizableText text) throws SAXException
834    {
835        _saxI18nizableText(handler, tagName, text, new AttributesImpl());
836    }
837    
838    private void _saxI18nizableText(TransformerHandler handler, String tagName, I18nizableText text, AttributesImpl attrs) throws SAXException
839    {
840        if (text != null)
841        {
842            attrs.addCDATAAttribute("i18n", Boolean.toString(text.isI18n()));
843            
844            String value = "";
845            
846            if (text.isI18n())
847            {
848                if (StringUtils.isNotBlank(text.getCatalogue()))
849                {
850                    value = text.getCatalogue() + ":";
851                }
852                value = value + text.getKey();
853            }
854            else
855            {
856                value = text.getLabel();
857            }
858            
859            XMLUtils.createElement(handler, tagName, attrs, value);
860        }
861    }
862
863    /**
864     * Determines if the content type can be removed
865     * @param contentTypeId The id of content type
866     * @return true if the content type can be removed
867     */
868    public boolean canRemoveContentType(String contentTypeId)
869    {
870        return _isAutomaticContentType(contentTypeId)
871                && _isLeafContentType(contentTypeId)
872                && !_isInUseContentType(contentTypeId);
873    }
874    
875    /**
876     * Remove a existing content type defined in WEB-INF/param/content-types directory.<br>
877     * This method archived the content type into the WEB-INF/param/content-types/_archives/pluginName folder.
878     * @param contentTypeId the id of content type to remove
879     * @throws RemoveContentTypeException if the content type could not be removed
880     */
881    public void removeContentType(String contentTypeId) throws RemoveContentTypeException
882    {
883        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
884        
885        if (contentType == null)
886        {
887            throw new RemoveContentTypeException("The content type with id '" + contentTypeId + "' can't be removed because this content type doesn't exist.");
888        }
889        
890        if (!_isAutomaticContentType(contentTypeId))
891        {
892            throw new RemoveContentTypeException("The content type with id '" + contentTypeId + "' can't be removed because it is not a modifiable content type");
893        }
894        
895        if (!_isLeafContentType(contentTypeId))
896        {
897            throw new RemoveContentTypeException("The content type with id '" + contentTypeId + "' can't be removed because it has one or more sub-types");
898        }
899        
900        if (_isInUseContentType(contentTypeId))
901        {
902            throw new RemoveContentTypeException("The content type with id '" + contentTypeId + "' can't be removed because it was currently in use");
903        }
904        
905        // Archive the content type
906        _archiveContentType(contentType);
907    }
908    
909    private void _archiveContentType(ContentType contentType) throws RemoveContentTypeException
910    {
911        try
912        {
913            File archiveRootDir = getArchiveRootDirectory(contentType);
914            
915            String realId = contentType.getId().replaceFirst(__CONTENT_TYPE_PREFIX, "");
916            File cTypeFile = getContentTypeFile(contentType);
917            
918            File destFile = new File(archiveRootDir, realId + ".xml");
919            
920            int index = 2;
921            while (destFile.exists())
922            {
923                destFile = new File(archiveRootDir, realId + "-" + index + ".xml");
924                index++;
925            }
926            
927            FileUtils.moveFile(cTypeFile, destFile);
928        }
929        catch (IOException e)
930        {
931            throw new RemoveContentTypeException("Fail to archive content type " + contentType.getId(), e);
932        }
933    }
934    
935    private boolean _isInUseContentType(String contentTypeId)
936    {
937        Expression cTypeExpr = new ContentTypeOrMixinExpression(Operator.EQ, contentTypeId);
938        String xPathQuery = ContentQueryHelper.getContentXPathQuery(cTypeExpr);
939        AmetysObjectIterable<Content> contents = _ametysObjectResolver.query(xPathQuery);
940        return contents.iterator().hasNext();
941    }
942
943    private boolean _isAutomaticContentType(String contentTypeId)
944    {
945        ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
946        return contentType instanceof AutomaticContentType;
947    }
948
949    private boolean _isLeafContentType(String contentTypeId)
950    {
951        return _contentTypeExtensionPoint.getDirectSubTypes(contentTypeId).size() == 0;
952    }
953
954}