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