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.runtime.model;
017
018import java.util.ArrayList;
019import java.util.Collections;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Objects;
025import java.util.Set;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.cocoon.ProcessingException;
030import org.apache.cocoon.xml.AttributesImpl;
031import org.xml.sax.ContentHandler;
032import org.xml.sax.SAXException;
033
034import org.ametys.core.util.XMLUtils;
035import org.ametys.runtime.config.DisableCondition;
036import org.ametys.runtime.config.DisableConditions;
037import org.ametys.runtime.i18n.I18nizableText;
038import org.ametys.runtime.model.checker.ItemCheckerDescriptor;
039
040/**
041 * Abstract class for model items
042 */
043public abstract class AbstractModelItem implements ModelItem
044{
045    /** The service manager */
046    protected static ServiceManager __serviceManager;
047    
048    private String _name;
049    private String _pluginName;
050    private I18nizableText _label;
051    private I18nizableText _description;
052    private Set<ItemCheckerDescriptor> _itemCheckers = new HashSet<>();
053    private String _widget;
054    private Map<String, I18nizableText> _widgetParams;
055    private DisableConditions _disableConditions;
056
057    private Model _model;
058    private ModelItemGroup _parent;
059    
060    private String _path;
061
062    /**
063     * Default constructor.
064     */
065    public AbstractModelItem()
066    {
067        // Empty constructor
068    }
069    
070    /**
071     * Constructor used to create simple models and items 
072     * @param name the name of the item
073     */
074    public AbstractModelItem(String name)
075    {
076        _name = name;
077    }
078
079    /**
080     * Constructor by copying an existing {@link AbstractModelItem}.
081     * @param modelItemToCopy The {@link AbstractModelItem} to copy
082     */
083    public AbstractModelItem(ModelItem modelItemToCopy)
084    {
085        setName(modelItemToCopy.getName());
086        setPluginName(modelItemToCopy.getPluginName());
087        setLabel(modelItemToCopy.getLabel());
088        setDescription(modelItemToCopy.getDescription());
089        for (ItemCheckerDescriptor itemChecker : modelItemToCopy.getItemCheckers())
090        {
091            addItemChecker(itemChecker);
092        }
093        setModel(modelItemToCopy.getModel());
094        setParent(modelItemToCopy.getParent());
095        
096        // Widget
097        setWidget(modelItemToCopy.getWidget());
098        setWidgetParameters(modelItemToCopy.getWidgetParameters());
099        
100        setDisableConditions(modelItemToCopy.getDisableConditions());
101    }
102    
103    public String getName()
104    {
105        return _name;
106    }
107    
108    public void setName(String name)
109    {
110        _name = name;
111        _path = null;
112    }
113    
114    public String getPluginName()
115    {
116        return _pluginName;
117    }
118
119    public void setPluginName(String pluginName)
120    {
121        _pluginName = pluginName;
122    }
123    
124    public I18nizableText getLabel()
125    {
126        return _label;
127    }
128
129    public void setLabel(I18nizableText label)
130    {
131        _label = label;
132    }
133
134    public I18nizableText getDescription()
135    {
136        return _description;
137    }
138
139    public void setDescription(I18nizableText description)
140    {
141        _description = description;
142    }
143    
144    public void addItemChecker(ItemCheckerDescriptor itemChecker)
145    {
146        _itemCheckers.add(itemChecker);
147    }
148    
149    public Set<ItemCheckerDescriptor> getItemCheckers()
150    {
151        return Collections.unmodifiableSet(_itemCheckers);
152    }
153    
154
155    public String getWidget()
156    {
157        return _widget;
158    }
159
160    public void setWidget(String widget)
161    {
162        _widget = widget;
163    }
164    
165    public Map<String, I18nizableText> getWidgetParameters()
166    {
167        return _widgetParams;
168    }
169    
170    public void setWidgetParameters (Map<String, I18nizableText> params)
171    {
172        _widgetParams = params;
173    }
174    
175    public DisableConditions getDisableConditions()
176    {
177        return _disableConditions;
178    }
179
180    public void setDisableConditions(DisableConditions disableConditions)
181    {
182        _disableConditions = disableConditions;
183    }
184
185    public String getPath()
186    {
187        if (_path != null)
188        {
189            return _path;
190        }
191        
192        if (getName() == null)
193        {
194            return null;
195        }
196        
197        StringBuilder path = new StringBuilder();
198        
199        ModelItemGroup parent = getParent();
200        if (parent != null && parent.getPath() != null)
201        {
202            path.append(parent.getPath()).append(ITEM_PATH_SEPARATOR);
203        }
204        
205        path.append(getName());
206        _path =  path.toString();
207        return _path;
208    }
209
210    public Model getModel()
211    {
212        return _model;
213    }
214    
215    public void setModel(Model model)
216    {
217        _model = model;
218    }
219
220    public ModelItemGroup getParent()
221    {
222        return _parent;
223    }
224    
225    public void setParent(ModelItemGroup parent)
226    {
227        _parent = parent;
228    }
229    
230    public Map<String, Object> toJSON(DefinitionContext context) throws ProcessingException
231    {
232        if (_shouldJSONBeEmpty(context))
233        {
234            return Map.of();
235        }
236        else
237        {
238            return _toJSON(context);
239        }
240    }
241    
242    /**
243     * Converts the model item in a JSON map
244     * @param context the context of the definition
245     * @return The model item as a JSON map
246     * @throws ProcessingException If an error occurs when converting the model item
247     */
248    protected Map<String, Object> _toJSON(DefinitionContext context) throws ProcessingException
249    {
250        Map<String, Object> result = new HashMap<>();
251        
252        result.put("name", getName());
253        result.put("plugin", getPluginName());
254        result.put("label", getLabel());
255        result.put("description", getDescription());
256        result.put("path", getPath());
257        
258        if (!getItemCheckers().isEmpty())
259        {
260            List<Map<String, Object>> checkers2json = new ArrayList<>();
261            for (ItemCheckerDescriptor paramChecker : getItemCheckers())
262            {
263                checkers2json.add(paramChecker.toJSON());
264            }
265            
266            result.put("field-checker", checkers2json);
267        }
268        
269        result.put("widget", getWidget());
270        
271        Map<String, I18nizableText> widgetParameters = getWidgetParameters();
272        if (widgetParameters != null && !widgetParameters.isEmpty())
273        {
274            result.put("widget-params", widgetParameters);
275        }
276
277        
278        if (getDisableConditions() != null)
279        {
280            result.put("disableCondition", _disableConditionstoJSON(getDisableConditions()));
281        }
282        
283        return result;
284    }
285    
286    /**
287     * Converts the definition's disable conditions in a JSON map
288     * @param disableConditions the disable conditions to convert
289     * @return The definition's disable conditions as a JSON map
290     */
291    private Map<String, Object> _disableConditionstoJSON(DisableConditions disableConditions)
292    {
293        Map<String, Object> map = new HashMap<>();
294        
295        // Handle simple conditions
296        List<Map<String, String>> disableConditionList = new ArrayList<>();
297        map.put("condition", disableConditionList);
298        for (DisableCondition disableCondition : disableConditions.getConditions())
299        {
300            Map<String, String> disableConditionAsMap = _disableConditiontoJSON(disableCondition);
301            disableConditionList.add(disableConditionAsMap);
302        }
303
304        // Handle nested conditions
305        List<Map<String, Object>> disableConditionsList = new ArrayList<>();
306        map.put("conditions", disableConditionsList);
307        for (DisableConditions subDisableConditions : disableConditions.getSubConditions())
308        {
309            Map<String, Object> disableConditionsAsMap = _disableConditionstoJSON(subDisableConditions);
310            disableConditionsList.add(disableConditionsAsMap);
311        }
312        
313        // Handle type
314        map.put("type", disableConditions.getAssociationType().toString().toLowerCase());
315        
316        return map; 
317    }
318
319    private static Map<String, String> _disableConditiontoJSON(DisableCondition disableCondition)
320    {
321        Map<String, String> map = new HashMap<>();
322        map.put("id", disableCondition.getId());
323        map.put("operator", disableCondition.getOperator().toString().toLowerCase());
324        map.put("value", disableCondition.getValue());
325        return map;
326    }
327    
328    /**
329     * Checks if the current definition JSON conversion should return an empty map
330     * @param context the context of the definition
331     * @return <code>true</code> if the JSON conversion should return an empty map, <code>false</code> otherwise
332     */
333    protected boolean _shouldJSONBeEmpty(DefinitionContext context)
334    {
335        return false;
336    }
337    
338    @SuppressWarnings("static-access")
339    public void toSAX(ContentHandler contentHandler, DefinitionContext context) throws SAXException
340    {
341        if (!getItemCheckers().isEmpty())
342        {
343            for (ItemCheckerDescriptor paramChecker : getItemCheckers())
344            {
345                XMLUtils.startElement(contentHandler, "field-checker");
346                paramChecker.toSAX(contentHandler);
347                XMLUtils.endElement(contentHandler, "field-checker");
348            }
349        }
350        
351        _widgetToSAX(contentHandler, context);
352
353        if (getDisableConditions() != null)
354        {
355            _disableConditionsToSAX(contentHandler, getDisableConditions());
356        }
357    }
358
359    /**
360     * Generates SAX events for the model item's widget
361     * @param contentHandler the {@link ContentHandler} that will receive the SAX events 
362     * @param context the context of the definition
363     * @throws SAXException if an error occurs during the SAX events generation
364     */
365    @SuppressWarnings("static-access")
366    protected void _widgetToSAX(ContentHandler contentHandler, DefinitionContext context) throws SAXException
367    {
368        XMLUtils.createElementIfNotNull(contentHandler, "widget", getWidget());
369        
370        Map<String, I18nizableText> widgetParameters = getWidgetParameters();
371        if (widgetParameters != null && !widgetParameters.isEmpty())
372        {
373            XMLUtils.startElement(contentHandler, "widget-params");
374            
375            for (Map.Entry<String, I18nizableText> param : widgetParameters.entrySet())
376            {
377                _widgetParameterToSAX(contentHandler, param.getKey(), param.getValue(), context);
378            }
379            
380            XMLUtils.endElement(contentHandler, "widget-params");
381        }
382    }
383
384    /**
385     * Generates SAX events for the given widget parameter
386     * @param contentHandler the {@link ContentHandler} that will receive the SAX events 
387     * @param parameterName the name of the parameter
388     * @param parameterValue the value of the parameter
389     * @param context the context of the definition
390     * @throws SAXException if an error occurs during the SAX events generation
391     */
392    @SuppressWarnings("static-access")
393    protected void _widgetParameterToSAX(ContentHandler contentHandler, String parameterName, I18nizableText parameterValue, DefinitionContext context) throws SAXException
394    {
395        AttributesImpl paramAttributes = new AttributesImpl();
396        paramAttributes.addCDATAAttribute("name", parameterName);
397
398        XMLUtils.startElement(contentHandler, "param", paramAttributes);
399        parameterValue.toSAX(contentHandler);
400        XMLUtils.endElement(contentHandler, "param");
401    }
402    
403    @SuppressWarnings("static-access")
404    private void _disableConditionsToSAX(ContentHandler contentHandler, DisableConditions disableConditions) throws SAXException
405    {
406        AttributesImpl attributes = new AttributesImpl();
407        attributes.addCDATAAttribute("type", disableConditions.getAssociationType().toString().toLowerCase());
408        XMLUtils.startElement(contentHandler, "disable-conditions", attributes);
409        
410        // Handle simple conditions
411        XMLUtils.startElement(contentHandler, "conditions");
412        for (DisableCondition disableCondition : disableConditions.getConditions())
413        {
414            _disableConditionToSAX(contentHandler, disableCondition);
415        }
416        XMLUtils.endElement(contentHandler, "conditions");
417
418        // Handle nested conditions
419        XMLUtils.startElement(contentHandler, "nested-conditions");
420        for (DisableConditions subDisableConditions : disableConditions.getSubConditions())
421        {
422            _disableConditionsToSAX(contentHandler, subDisableConditions);
423        }
424        XMLUtils.endElement(contentHandler, "nested-conditions");
425        
426        XMLUtils.endElement(contentHandler, "disable-conditions");
427    }
428    
429    @SuppressWarnings("static-access")
430    private void _disableConditionToSAX(ContentHandler contentHandler, DisableCondition disableCondition) throws SAXException
431    {
432        AttributesImpl attributes = new AttributesImpl();
433        attributes.addCDATAAttribute("id", disableCondition.getId());
434        attributes.addCDATAAttribute("operator", disableCondition.getOperator().toString().toLowerCase());
435        
436        XMLUtils.createElement(contentHandler, "condition", attributes, disableCondition.getValue());
437    }
438    
439    public int compareTo(ModelItem item)
440    {
441        if (item == null)
442        {
443            return 1;
444        }
445        
446        String name = getName();
447        if (name != null)
448        {
449            return name.compareTo(item.getName());
450        }
451        
452        I18nizableText label = getLabel();
453        if (label != null)
454        {
455            I18nizableText otherLabel = item.getLabel();
456            if (otherLabel == null)
457            {
458                return 1;
459            }
460            
461            return label.toString().compareTo(otherLabel.toString());
462        }
463        
464        return 0;
465    }
466    
467    @Override
468    public boolean equals(Object obj)
469    {
470        if (obj == null || !getClass().equals(obj.getClass()))
471        {
472            return false;
473        }
474        
475        ModelItem item = (ModelItem) obj;
476        
477        // The item's model can be null
478        if (getModel() != item.getModel())
479        {
480            if (getModel() == null ^ item.getModel() == null)
481            {
482                return false;
483            }
484            
485            if (!Objects.equals(getModel().getFamilyId(), item.getModel().getFamilyId()) || !Objects.equals(getModel().getId(), item.getModel().getId()))
486            {
487                return false;
488            }
489        }
490        
491        if (getPath() != null || item.getPath() != null)
492        {
493            return Objects.equals(getPath(), item.getPath());
494        }
495        
496        if (getLabel() != null || item.getLabel() != null)
497        {
498            return Objects.equals(getLabel(), item.getLabel());
499        }
500        
501        return false;
502    }
503
504    @Override
505    public int hashCode()
506    {
507        if (getPath() != null)
508        {
509            return getPath().hashCode();
510        }
511        
512        return getLabel().hashCode();
513    }
514    
515    @Override
516    public String toString()
517    {
518        if (getPath() != null)
519        {
520            return getPath();
521        }
522        
523        return getLabel().toString();
524    }
525    
526    /**
527     * Set the service manager
528     * The {@link ServiceManager} is used in the model items creation methods to get the model item type.
529     * {@link ModelItem} is not a {@link Component} and can't have a {@link ServiceManager} itself. Another {@link Component} has to set it
530     * @param manager the service manager to set
531     */
532    public static void setServiceManager(ServiceManager manager)
533    {
534        __serviceManager = manager;
535    }
536}