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        result.put("name", getName());
259        result.put("plugin", getPluginName());
260        result.put("label", getLabel());
261        result.put("description", getDescription());
262        result.put("path", getPath());
263        
264        if (!getItemCheckers().isEmpty())
265        {
266            List<Map<String, Object>> checkers2json = new ArrayList<>();
267            for (ItemCheckerDescriptor paramChecker : getItemCheckers())
268            {
269                checkers2json.add(paramChecker.toJSON());
270            }
271            
272            result.put("field-checker", checkers2json);
273        }
274        
275        result.putAll(_widgetToJSON(context));
276        
277        if (ModelHelper.hasDisableConditions(this))
278        {
279            result.put("disableCondition", disableConditionsToJSON());
280        }
281        
282        return result;
283    }
284    
285    /**
286     * Converts the model item's widget in a JSON map
287     * @param context the context of the definition
288     * @return The model item's widget as a JSON map
289     */
290    protected Map<String, Object> _widgetToJSON(DefinitionContext context)
291    {
292        Map<String, Object> result = new HashMap<>();
293        
294        result.put("widget", getWidget());
295        
296        Map<String, I18nizableText> widgetParameters = getWidgetParameters();
297        if (widgetParameters != null && !widgetParameters.isEmpty())
298        {
299            result.put("widget-params", widgetParameters);
300        }
301        
302        return result;
303    }
304    
305    /**
306     * Converts the definition's disable conditions in a JSON map
307     * @return The definition's disable conditions as a JSON map
308     */
309    protected Map<String, Object> disableConditionsToJSON()
310    {
311        return _getModelItemHelper().disableConditionsToJSON(this);
312    }
313
314    /**
315     * Checks if the current definition JSON conversion should return an empty map
316     * @param context the context of the definition
317     * @return <code>true</code> if the JSON conversion should return an empty map, <code>false</code> otherwise
318     */
319    protected boolean _shouldJSONBeEmpty(DefinitionContext context)
320    {
321        return false;
322    }
323    
324    @SuppressWarnings("static-access")
325    public void toSAX(ContentHandler contentHandler, DefinitionContext context) throws SAXException
326    {
327        if (!getItemCheckers().isEmpty())
328        {
329            for (ItemCheckerDescriptor paramChecker : getItemCheckers())
330            {
331                XMLUtils.startElement(contentHandler, "field-checker");
332                paramChecker.toSAX(contentHandler);
333                XMLUtils.endElement(contentHandler, "field-checker");
334            }
335        }
336        
337        _widgetToSAX(contentHandler, context);
338
339        if (getDisableConditions() != null)
340        {
341            _disableConditionsToSAX(contentHandler, getDisableConditions());
342        }
343    }
344
345    /**
346     * Generates SAX events for the model item's widget
347     * @param contentHandler the {@link ContentHandler} that will receive the SAX events 
348     * @param context the context of the definition
349     * @throws SAXException if an error occurs during the SAX events generation
350     */
351    @SuppressWarnings("static-access")
352    protected void _widgetToSAX(ContentHandler contentHandler, DefinitionContext context) throws SAXException
353    {
354        XMLUtils.createElementIfNotNull(contentHandler, "widget", getWidget());
355        
356        Map<String, I18nizableText> widgetParameters = getWidgetParameters();
357        if (widgetParameters != null && !widgetParameters.isEmpty())
358        {
359            XMLUtils.startElement(contentHandler, "widget-params");
360            
361            for (Map.Entry<String, I18nizableText> param : widgetParameters.entrySet())
362            {
363                _widgetParameterToSAX(contentHandler, param.getKey(), param.getValue(), context);
364            }
365            
366            XMLUtils.endElement(contentHandler, "widget-params");
367        }
368    }
369
370    /**
371     * Generates SAX events for the given widget parameter
372     * @param contentHandler the {@link ContentHandler} that will receive the SAX events 
373     * @param parameterName the name of the parameter
374     * @param parameterValue the value of the parameter
375     * @param context the context of the definition
376     * @throws SAXException if an error occurs during the SAX events generation
377     */
378    @SuppressWarnings("static-access")
379    protected void _widgetParameterToSAX(ContentHandler contentHandler, String parameterName, I18nizableText parameterValue, DefinitionContext context) throws SAXException
380    {
381        AttributesImpl paramAttributes = new AttributesImpl();
382        paramAttributes.addCDATAAttribute("name", parameterName);
383
384        XMLUtils.startElement(contentHandler, "param", paramAttributes);
385        parameterValue.toSAX(contentHandler);
386        XMLUtils.endElement(contentHandler, "param");
387    }
388    
389    @SuppressWarnings("static-access")
390    private void _disableConditionsToSAX(ContentHandler contentHandler, DisableConditions disableConditions) throws SAXException
391    {
392        AttributesImpl attributes = new AttributesImpl();
393        attributes.addCDATAAttribute("type", disableConditions.getAssociationType().toString().toLowerCase());
394        XMLUtils.startElement(contentHandler, "disable-conditions", attributes);
395        
396        // Handle simple conditions
397        XMLUtils.startElement(contentHandler, "conditions");
398        for (DisableCondition disableCondition : disableConditions.getConditions())
399        {
400            _disableConditionToSAX(contentHandler, disableCondition);
401        }
402        XMLUtils.endElement(contentHandler, "conditions");
403
404        // Handle nested conditions
405        XMLUtils.startElement(contentHandler, "nested-conditions");
406        for (DisableConditions subDisableConditions : disableConditions.getSubConditions())
407        {
408            _disableConditionsToSAX(contentHandler, subDisableConditions);
409        }
410        XMLUtils.endElement(contentHandler, "nested-conditions");
411        
412        XMLUtils.endElement(contentHandler, "disable-conditions");
413    }
414    
415    @SuppressWarnings("static-access")
416    private void _disableConditionToSAX(ContentHandler contentHandler, DisableCondition disableCondition) throws SAXException
417    {
418        AttributesImpl attributes = new AttributesImpl();
419        attributes.addCDATAAttribute("id", disableCondition.getId());
420        attributes.addCDATAAttribute("operator", disableCondition.getOperator().toString().toLowerCase());
421        
422        XMLUtils.createElement(contentHandler, "condition", attributes, disableCondition.getValue());
423    }
424    
425    public int compareTo(ModelItem item)
426    {
427        if (item == null)
428        {
429            return 1;
430        }
431        
432        String name = getName();
433        if (name != null)
434        {
435            return name.compareTo(item.getName());
436        }
437        
438        I18nizableText label = getLabel();
439        if (label != null)
440        {
441            I18nizableText otherLabel = item.getLabel();
442            if (otherLabel == null)
443            {
444                return 1;
445            }
446            
447            return label.toString().compareTo(otherLabel.toString());
448        }
449        
450        return 0;
451    }
452    
453    @Override
454    public boolean equals(Object obj)
455    {
456        if (obj == null || !getClass().equals(obj.getClass()))
457        {
458            return false;
459        }
460        
461        ModelItem item = (ModelItem) obj;
462        
463        // The item's model can be null
464        if (getModel() != item.getModel())
465        {
466            if (getModel() == null ^ item.getModel() == null)
467            {
468                return false;
469            }
470            
471            if (!Objects.equals(getModel().getFamilyId(), item.getModel().getFamilyId()) || !Objects.equals(getModel().getId(), item.getModel().getId()))
472            {
473                return false;
474            }
475        }
476        
477        if (getPath() != null || item.getPath() != null)
478        {
479            return Objects.equals(getPath(), item.getPath());
480        }
481        
482        if (getLabel() != null || item.getLabel() != null)
483        {
484            return Objects.equals(getLabel(), item.getLabel());
485        }
486        
487        return false;
488    }
489
490    @Override
491    public int hashCode()
492    {
493        if (getPath() != null)
494        {
495            return getPath().hashCode();
496        }
497        
498        return getLabel().hashCode();
499    }
500    
501    @Override
502    public String toString()
503    {
504        if (getPath() != null)
505        {
506            return getPath();
507        }
508        
509        return getLabel().toString();
510    }
511    
512    /**
513     * Retrieves the {@link ModelItemHelper} 
514     * @return the {@link ModelItemHelper}
515     */
516    protected ModelItemHelper _getModelItemHelper()
517    {
518        if (_modelItemHelper == null)
519        {
520            try
521            {
522                _modelItemHelper = (ModelItemHelper) __serviceManager.lookup(ModelItemHelper.ROLE);
523            }
524            catch (ServiceException e)
525            {
526                throw new RuntimeException("Unable to lookup after the model item helper", e);
527            }
528        }
529        
530        return _modelItemHelper;
531    }
532    
533    /**
534     * Set the service manager
535     * The {@link ServiceManager} is used in the model items creation methods to get the model item type.
536     * {@link ModelItem} is not a {@link Component} and can't have a {@link ServiceManager} itself. Another {@link Component} has to set it
537     * @param manager the service manager to set
538     */
539    public static void setServiceManager(ServiceManager manager)
540    {
541        __serviceManager = manager;
542    }
543    
544    /**
545     * Set the cocoon context
546     * {@link ModelItem} is not a {@link Component} and can't have a {@link Context} itself. Another {@link Component} has to set it
547     * @param context the cocoon context to set
548     */
549    public static void setContext(Context context)
550    {
551        __context = context;
552    }
553}