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