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