001/*
002 *  Copyright 2020 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.IOException;
019import java.io.InputStream;
020import java.util.Optional;
021
022import org.apache.avalon.framework.component.Component;
023import org.apache.avalon.framework.configuration.Configuration;
024import org.apache.avalon.framework.configuration.ConfigurationException;
025import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
026import org.apache.avalon.framework.logger.AbstractLogEnabled;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.commons.lang3.StringUtils;
031import org.apache.excalibur.source.Source;
032import org.apache.excalibur.source.SourceResolver;
033import org.xml.sax.SAXException;
034
035import org.ametys.runtime.model.ElementDefinition;
036import org.ametys.runtime.model.ModelItem;
037import org.ametys.runtime.model.ModelItemGroup;
038import org.ametys.runtime.model.ModelViewItem;
039import org.ametys.runtime.model.ModelViewItemGroup;
040import org.ametys.runtime.model.SimpleViewItemGroup;
041import org.ametys.runtime.model.TemporaryViewReference;
042import org.ametys.runtime.model.View;
043import org.ametys.runtime.model.ViewElement;
044import org.ametys.runtime.model.ViewElementAccessor;
045import org.ametys.runtime.model.ViewItemAccessor;
046import org.ametys.runtime.model.ViewItemGroup;
047import org.ametys.runtime.model.ViewParser;
048
049/**
050 * Component that parses the configuration of a content type's view
051 */
052public class ContentTypeViewParser extends AbstractLogEnabled implements ViewParser, Component, Serviceable
053{
054    /** Default tag name for references to attributes */
055    public static final String DEFAULT_ATTRIBUTE_REF_TAG_NAME = "attribute-ref";
056    
057    /** The content type extension point */
058    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
059    /** The content types helper */
060    protected ContentTypesHelper _contentTypesHelper;
061    /** The content types parser helper */
062    protected ContentTypesParserHelper _contentTypesParserHelper;
063    /** The source resolver */
064    protected SourceResolver _srcResolver;
065
066    /** The content type containing view */
067    protected ContentType _contentType;
068    
069    /** The tag name for simple groups */
070    protected String _groupTagName;
071    /** The tag name for attribute references */
072    protected String _attributeRefTagName;
073    /** Determine if this parser supports attribute nested in an attribute of type content */
074    protected boolean _supportsNestedAttributes;
075    
076    /**
077     * Creates a view parser for content types
078     * @param contentType the content type containing the view
079     */
080    public ContentTypeViewParser(ContentType contentType)
081    {
082        this(contentType, Optional.empty(), Optional.empty(), true);
083    }
084    
085    /**
086     * Creates a view parser for content types
087     * @param contentType the content type containing the view
088     * @param groupTagName the tag name for simple groups
089     * @param attributeRefTagName the tag name for attribute references
090     * @param supportsNestedAttributes <code>false</code> to avoid this parser to support nested attributes
091     */
092    public ContentTypeViewParser(ContentType contentType, Optional<String> groupTagName, Optional<String> attributeRefTagName, boolean supportsNestedAttributes)
093    {
094        _contentType = contentType;
095        _groupTagName = groupTagName.orElse(DEFAULT_GROUP_TAG_NAME);
096        _attributeRefTagName = attributeRefTagName.orElse(DEFAULT_ATTRIBUTE_REF_TAG_NAME);
097        _supportsNestedAttributes = supportsNestedAttributes;
098    }
099    
100    public void service(ServiceManager manager) throws ServiceException
101    {
102        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) manager.lookup(ContentTypeExtensionPoint.ROLE);
103        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
104        _contentTypesParserHelper = (ContentTypesParserHelper) manager.lookup(ContentTypesParserHelper.ROLE);
105        _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
106    }
107
108    public View parseView(Configuration viewConfiguration) throws ConfigurationException
109    {
110        View view = new View();
111
112        String name = viewConfiguration.getAttribute("name");
113        view.setName(name);
114        view.setInternal(viewConfiguration.getAttributeAsBoolean("internal", false));
115        view.setLabel(_contentTypesParserHelper.parseI18nizableText(_contentType, viewConfiguration, "label", name));
116        view.setDescription(_contentTypesParserHelper.parseI18nizableText(_contentType, viewConfiguration, "description"));
117        
118        Configuration iconConf = viewConfiguration.getChild("icons");
119        
120        view.setSmallIcon(_contentTypesParserHelper.parseIcon(_contentType, iconConf, "small", null));
121        String mediumIcon = _contentTypesParserHelper.parseIcon(_contentType, iconConf, "medium", null);
122        view.setMediumIcon(mediumIcon);
123        view.setLargeIcon(_contentTypesParserHelper.parseIcon(_contentType, iconConf, "large", null));
124        
125        view.setIconGlyph(_contentTypesParserHelper.parseIconGlyph(iconConf, name, mediumIcon == null ? "ametysicon-column3" : null));
126        view.setIconDecorator(iconConf.getChild("decorator").getValue(null));
127
128        _fillViewItems(viewConfiguration, view);
129
130        return view;
131    }
132    
133    /**
134     * Fill the items of the given view
135     * @param viewConfiguration the configuration of the view to fill
136     * @param view the view to fill
137     * @throws ConfigurationException if the configuration is not valid. 
138     */
139    protected void _fillViewItems(Configuration viewConfiguration, View view) throws ConfigurationException
140    {
141        for (Configuration itemConfiguration : viewConfiguration.getChildren())
142        {
143            String itemConfigurationName = itemConfiguration.getName();
144            
145            if (_attributeRefTagName.equals(itemConfigurationName))
146            {
147                ModelViewItem viewItem = _parseModelViewItem(itemConfiguration, StringUtils.EMPTY, view);
148
149                if (getLogger().isWarnEnabled() && view.hasModelViewItem(viewItem))
150                {
151                    String itemName = viewItem.getDefinition().getName();
152                    getLogger().warn("The item '" + itemName + "' is already referenced by a super-view or by the view '" + view.getName() + "' itself.");
153                }
154
155                view.addViewItem(viewItem);
156                
157            }
158            else if (_groupTagName.equals(itemConfigurationName))
159            {
160                view.addViewItem(_parseSimpleViewItemGroup(itemConfiguration, StringUtils.EMPTY, ViewItemGroup.TAB_ROLE, view));
161            }
162            else if ("dublin-core".equals(itemConfigurationName))
163            {
164                view.addViewItem(_parseDublinCoreViewItems());
165            }
166            else if ("include".equals(itemConfigurationName))
167            {
168                view.includeView(_getViewToInclude(itemConfiguration, view.getName()));
169            }
170        }
171    }
172
173    /**
174     * Parses a model view item
175     * @param itemConfiguration configuration of the model view item
176     * @param parentPath path of the parent of the model view item
177     * @param referenceView view that references the item
178     * @return the model view item
179     * @throws ConfigurationException if the configuration is not valid.
180     */
181    protected ModelViewItem _parseModelViewItem(Configuration itemConfiguration, String parentPath, View referenceView) throws ConfigurationException
182    {
183        String modelItemName = itemConfiguration.getAttribute("name");
184        String path = StringUtils.isEmpty(parentPath) ? modelItemName : parentPath + ModelItem.ITEM_PATH_SEPARATOR + modelItemName;
185        
186        if (!_contentType.hasModelItem(path))
187        {
188            throw new ConfigurationException("The item '" + path + "' in the view '" + referenceView.getName() + "' is not defined in content type '" + _contentType.getId() + "'", itemConfiguration);
189        }
190
191        ModelItem modelItem = _contentType.getModelItem(path);
192        ModelViewItem viewItem;
193        if (modelItem instanceof ModelItemGroup)
194        {
195            if (_shouldInsertAllAccessorItems(itemConfiguration))
196            {
197                viewItem = ModelViewItemGroup.of((ModelItemGroup) modelItem);
198            }
199            else
200            {
201                viewItem = new ModelViewItemGroup();
202                _parseViewItemAccessorChildren(itemConfiguration, (ViewItemAccessor) viewItem, path, referenceView);
203            }
204            
205            ((ModelViewItemGroup) viewItem).setDefinition((ModelItemGroup) modelItem);
206        }
207        else if (modelItem instanceof ContentAttributeDefinition)
208        {
209            viewItem = _parseContentAttributeViewItem(itemConfiguration, (ContentAttributeDefinition) modelItem, path, referenceView);
210            ((ViewElement) viewItem).setDefinition((ElementDefinition) modelItem);
211        }
212        else
213        {
214            viewItem = new ViewElement();
215            ((ViewElement) viewItem).setDefinition((ElementDefinition) modelItem);
216        }
217        
218        if (itemConfiguration.getChild("label", false) != null)
219        {
220            viewItem.setLabel(_contentTypesParserHelper.parseI18nizableText(_contentType, itemConfiguration, "label"));
221        }
222        
223        if (itemConfiguration.getChild("description", false) != null)
224        {
225            viewItem.setDescription(_contentTypesParserHelper.parseI18nizableText(_contentType, itemConfiguration, "description"));
226        }
227        
228        return viewItem;
229    }
230    
231    /**
232     * Parses the view item referencing an attribute of type content 
233     * @param itemConfiguration configuration of the view item
234     * @param definition definition of the attribute
235     * @param path path of the item in the view
236     * @param referenceView view that references the item
237     * @return the view item
238     * @throws ConfigurationException if the configuration is not valid
239     */
240    protected ViewElement _parseContentAttributeViewItem(Configuration itemConfiguration, ContentAttributeDefinition definition, String path, View referenceView) throws ConfigurationException
241    {
242        if (_supportsNestedAttributes)
243        {
244            ViewElementAccessor viewItem = new ViewElementAccessor();
245
246            if (_shouldInsertAllAccessorItems(itemConfiguration))
247            {
248                String contentTypeId = definition.getContentTypeId();
249                if (contentTypeId != null)
250                {
251                    ContentType contentType = _contentTypeExtensionPoint.getExtension(contentTypeId);
252                    // Create a view with all content type's items
253                    View view = View.of(contentType);
254                    // And add all items to the current accessor
255                    viewItem.addViewItems(view.getViewItems());
256                }
257                else
258                {
259                    // No content type is defined on the attribute, add a view item for title
260                    ViewElement titleViewElement = new ViewElement();
261                    titleViewElement.setDefinition(_contentTypesHelper.getTitleAttributeDefinition());
262                    viewItem.addViewItem(titleViewElement);
263                }
264            }
265            else
266            {
267                // Parse children
268                _parseViewItemAccessorChildren(itemConfiguration, viewItem, path, referenceView);
269            }
270            
271            return viewItem;
272        }
273        else
274        {
275            return new ViewElement();
276        }
277    }
278    
279    /**
280     * Checks if all the items of the current accessor should be inserted in the view
281     * @param accessorConfiguration configuration of the current accessor
282     * @return <code>true</code> if all items of the current accessor should be inserted in the view, <code>false</code> otherwise
283     * @throws ConfigurationException if the configuration is not valid
284     */
285    protected boolean _shouldInsertAllAccessorItems(Configuration accessorConfiguration) throws ConfigurationException
286    {
287        Configuration child = accessorConfiguration.getChild(_attributeRefTagName, false);
288        if (child != null)
289        {
290            String name = child.getAttribute("name");
291            return ALL_ITEMS_REFERENCE.equals(name);
292        }
293        else
294        {
295            return false;
296        }
297    }
298    
299    /**
300     * Parses the items of the given {@link ViewItemAccessor}
301     * @param itemConfiguration configuration of the model view item
302     * @param viewItemAccessor the {@link ViewItemAccessor} that will access to the parsed items
303     * @param modelItemPath path of the model view item.
304     * @param referenceView The view that references the item
305     * @throws ConfigurationException if the configuration is not valid
306     */
307    protected void _parseViewItemAccessorChildren(Configuration itemConfiguration, ViewItemAccessor viewItemAccessor, String modelItemPath, View referenceView) throws ConfigurationException
308    {
309        for (Configuration childConfiguration : itemConfiguration.getChildren())
310        {
311            String childConfigurationName = childConfiguration.getName();
312            
313            if (_attributeRefTagName.equals(childConfigurationName))
314            {
315                ModelViewItem viewItem = _parseModelViewItem(childConfiguration, modelItemPath, referenceView);
316
317                if (getLogger().isWarnEnabled() && (viewItemAccessor.hasModelViewItem(viewItem) || referenceView.hasModelViewItem(viewItem)))
318                {
319                    String itemPath = viewItem.getDefinition().getPath();
320                    getLogger().warn("The item '" + itemPath + "' is already referenced by the accessor '" + itemConfiguration.getAttribute("name") + "' or by the view '" + referenceView.getName() + ".");
321                }
322
323                viewItemAccessor.addViewItem(viewItem);
324                
325            }
326            else if (_groupTagName.equals(childConfigurationName))
327            {
328                viewItemAccessor.addViewItem(_parseSimpleViewItemGroup(childConfiguration, modelItemPath, ViewItemGroup.FIELDSET_ROLE, referenceView));
329            }
330            else if ("view".equals(childConfigurationName) && viewItemAccessor instanceof ViewElement)
331            {
332                // Insert items of the given view
333                String viewName = childConfiguration.getAttribute("name");
334                TemporaryViewReference viewReference = new TemporaryViewReference();
335                viewReference.setName(viewName);
336                viewItemAccessor.addViewItem(viewReference);
337            }
338            else
339            {
340                getLogger().error("The group '" + itemConfiguration.getAttribute("name") + "' contains forbidden items @ " + childConfiguration.getLocation());
341            }
342        }
343    }
344    
345    /**
346     * Parses a simple view item group
347     * @param itemConfiguration configuration of the simple view item group
348     * @param parentPath path of the model item parent of the group. 
349     * @param role role of the view group
350     * @param referenceView the reference view for includes
351     * @return the simple view item group
352     * @throws ConfigurationException if the configuration is not valid.
353     */
354    protected SimpleViewItemGroup _parseSimpleViewItemGroup(Configuration itemConfiguration, String parentPath, String role, View referenceView) throws ConfigurationException
355    {
356        SimpleViewItemGroup group = new SimpleViewItemGroup();
357        group.setRole(itemConfiguration.getAttribute("role", role));
358        group.setName(itemConfiguration.getAttribute("name", null));
359        
360        group.setLabel(_contentTypesParserHelper.parseI18nizableText(_contentType, itemConfiguration, "label"));
361        group.setDescription(_contentTypesParserHelper.parseI18nizableText(_contentType, itemConfiguration, "description"));
362
363        for (Configuration childConfiguration : itemConfiguration.getChildren())
364        {
365            String childConfigurationName = childConfiguration.getName();
366            
367            if (_attributeRefTagName.equals(childConfigurationName))
368            {
369                ModelViewItem viewItem = _parseModelViewItem(childConfiguration, parentPath, referenceView);
370                
371                if (getLogger().isWarnEnabled() && (group.hasModelViewItem(viewItem) || referenceView.hasModelViewItem(viewItem)))
372                {
373                    String itemPath = viewItem.getDefinition().getPath();
374                    String groupName = itemConfiguration.getAttribute("name", null);
375                    String groupDesignation = groupName != null ? "group named '" + groupName + "'" : "unnamed group";
376                    getLogger().warn("The item '" + itemPath + "' is already referenced by the current '" + groupDesignation + "' or by the view '" + referenceView.getName() + ".");
377                }
378                
379                group.addViewItem(viewItem);
380            }
381            else if (_groupTagName.equals(childConfigurationName))
382            {
383                group.addViewItem(_parseSimpleViewItemGroup(childConfiguration, parentPath, ViewItemGroup.FIELDSET_ROLE, referenceView));
384            }
385            else if ("dublin-core".equals(childConfigurationName))
386            {
387                group.addViewItem(_parseDublinCoreViewItems());
388            }
389            else if ("include".equals(childConfigurationName))
390            {
391                group.includeView(_getViewToInclude(childConfiguration, referenceView.getName()), referenceView);
392            }
393        }
394        
395        return group;
396    }
397    
398    /**
399     * Retrieves the view included by the given configuration
400     * @param itemConfiguration configuration of the item that includes the view
401     * @param viewName name of the view containing the item. (the item can be the view itself or a simple view item group inside the view)
402     * @return the view included by the given configuration
403     * @throws ConfigurationException if the configuration is not valid
404     */
405    protected View _getViewToInclude(Configuration itemConfiguration, String viewName) throws ConfigurationException
406    {
407        String superTypeId = itemConfiguration.getAttribute("from-supertype");
408        if ("true".equals(superTypeId))
409        {
410            View viewToInclude = _contentTypesHelper.getView(viewName, _contentType.getSupertypeIds(), new String[0]);
411            if (viewToInclude == null)
412            {
413                throw new ConfigurationException("The view '" + viewName + "' in content type '" + _contentType.getId() + "' includes a super-view not found in its super-types.", itemConfiguration);
414            }
415            return viewToInclude;
416        }
417        else
418        {
419            if (!_contentTypesHelper.getAncestors(_contentType.getId()).contains(superTypeId))
420            {
421                throw new ConfigurationException("The view '" + viewName + "' in content type '" + _contentType.getId() + "' includes the super-view of the type '" + superTypeId + "' that is not a super-type.", itemConfiguration);
422            }
423            
424            ContentType superType = _contentTypeExtensionPoint.getExtension(superTypeId);
425            if (superType == null)
426            {
427                throw new ConfigurationException("The view '" + viewName + "' in content type '" + _contentType.getId() + "' includes the super-view of an unknown super-type '" + superTypeId + "'.", itemConfiguration);
428            }
429            
430            View viewToInclude = superType.getView(viewName);
431            if (viewToInclude == null)
432            {
433                throw new ConfigurationException("The view '" + viewName + "' in content type '" + _contentType.getId() + "' includes the super-view not found in its super-type '" + superTypeId + "'.", itemConfiguration);
434            }
435            return viewToInclude;
436        }
437    }
438
439    /**
440     * Parses the DublinCore view items.
441     * @return the group containing the dublin core view items
442     * @throws ConfigurationException if the configuration is not valid.
443     */
444    protected ModelViewItemGroup _parseDublinCoreViewItems() throws ConfigurationException
445    {
446        ModelViewItemGroup dublinCoreGroup = new ModelViewItemGroup();
447        
448        ModelItemGroup dublinCoreRootDef = null;
449        if (_contentType.hasModelItem("/dc"))
450        {
451            ModelItem modelItem = _contentType.getModelItem("/dc");
452            if (modelItem instanceof ModelItemGroup)
453            {
454                dublinCoreRootDef = (ModelItemGroup) modelItem;
455            }
456        }
457        
458        if (dublinCoreRootDef != null)
459        {
460            dublinCoreGroup.setDefinition(dublinCoreRootDef);
461        }
462        else
463        {
464            throw new ConfigurationException("Unable to include Dublin Core view in a view of the content type '" + _contentType.getId() + "'. The Dublin Core attributes have not been defined.");
465        }
466        
467        
468        Source src = null;
469        
470        try
471        {
472            src = _srcResolver.resolveURI("resource://org/ametys/cms/dublincore/dublincore.xml");
473            
474            if (src.exists())
475            {
476                try (InputStream is = src.getInputStream())
477                {
478                    Configuration configuration = new DefaultConfigurationBuilder(true).build(is);
479                    Configuration viewConfiguration = configuration.getChild("metadata-set");
480                    
481                    for (Configuration childConfiguration : viewConfiguration.getChildren("metadata-ref"))
482                    {
483                        ViewElement viewElement = new ViewElement();
484                        String childAttributeName = "/dc" + ModelItem.ITEM_PATH_SEPARATOR + childConfiguration.getAttribute("name");
485                        if (_contentType.hasModelItem(childAttributeName))
486                        {
487                            ModelItem definition = _contentType.getModelItem(childAttributeName);
488                            if (definition instanceof ElementDefinition)
489                            {
490                                viewElement.setDefinition((ElementDefinition) definition);
491                                dublinCoreGroup.addViewItem(viewElement);
492                            }
493                            else
494                            {
495                                throw new ConfigurationException("Unable to include Dublin Core view in a view of the content type '" + _contentType.getId() + "'. The Dublin Core attribute '" + childAttributeName + "' has not been defined.");
496                            }
497                        }
498                        else
499                        {
500                            throw new ConfigurationException("Unable to include Dublin Core view in a view of the content type '" + _contentType.getId() + "'. The Dublin Core attribute '" + childAttributeName + "' has not been defined.");
501                        }
502                    }
503                }
504            }
505        }
506        catch (IOException | SAXException e)
507        {
508            throw new ConfigurationException("Unable to parse Dublin Core view", e);
509        }
510        finally
511        {
512            if (src != null)
513            {
514                _srcResolver.release(src);
515            }
516        }
517        
518        return dublinCoreGroup;
519    }
520}