001/*
002 *  Copyright 2014 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.parameter;
017
018import java.io.InputStream;
019import java.time.LocalDateTime;
020import java.time.ZonedDateTime;
021import java.time.chrono.IsoChronology;
022import java.time.format.DateTimeFormatter;
023import java.time.format.ResolverStyle;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Date;
027import java.util.HashMap;
028import java.util.List;
029import java.util.Map;
030
031import org.apache.cocoon.ProcessingException;
032import org.apache.cocoon.xml.AttributesImpl;
033import org.apache.cocoon.xml.XMLUtils;
034import org.slf4j.Logger;
035import org.slf4j.LoggerFactory;
036import org.xml.sax.ContentHandler;
037import org.xml.sax.SAXException;
038
039import org.ametys.core.util.DateUtils;
040import org.ametys.runtime.i18n.I18nizableText;
041
042
043/**
044 * This class handles all needed to use typed parameters
045 */
046public final class ParameterHelper
047{
048    /** Enumeration of supported types */
049    public static enum ParameterType
050    {
051        /** boolean values */
052        BOOLEAN,
053        /** string values */
054        STRING,
055        /** password values */
056        PASSWORD,
057        /** long values */
058        LONG,
059        /** double values */
060        DOUBLE,
061        /** date values */
062        DATE,
063        /** binary values */
064        BINARY,
065        /** datasource values */
066        DATASOURCE
067    }
068    
069    /**
070     * The ISO date-time formatter that formats or parses a date-time with an offset, such as '2011-12-03T10:15:30.000+01:00'. 
071     */
072    private static DateTimeFormatter __ISO_OFFSET_DATE_TIME = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss.SSSXXX").withResolverStyle(ResolverStyle.STRICT).withChronology(IsoChronology.INSTANCE);
073
074    // Logger for traces
075    private static Logger _logger = LoggerFactory.getLogger(ParameterHelper.class);
076
077    private ParameterHelper ()
078    {
079        // empty
080    }
081    
082    /**
083     * Get the ISO date-time formatter that formats or parses a date-time with an offset, such as '2011-12-03T10:15:30.000+01:00'. 
084     * This formatter is similar to {@link DateTimeFormatter#ISO_OFFSET_DATE_TIME} but force 3-digits milliseconds.
085     * @return ISO date-time formatter
086     */
087    public static DateTimeFormatter getISODateTimeFormatter()
088    {
089        return __ISO_OFFSET_DATE_TIME;
090    }
091    
092    /**
093     * Return the readable name of a type
094     * 
095     * @param type Type to convert
096     * @return Returns the name of the type
097     * @throws IllegalArgumentException If the type is unknwon
098     */
099    public static String typeToString(ParameterType type)
100    {
101        return type.name().toLowerCase();
102    }
103    
104    /**
105     * Convert a string containing a type to its value
106     * 
107     * @param type Name of the type
108     * @return Type
109     * @throws IllegalArgumentException if the type is unknown
110     */
111    public static ParameterType stringToType(String type)
112    {
113        try
114        {
115            return ParameterType.valueOf(type.toUpperCase());
116        }
117        catch (IllegalArgumentException e)
118        {
119            throw new IllegalArgumentException("The type '" + type + "' is unknown for config parameters");
120        }
121    }
122    
123    /**
124     * Cast a untyped value (string) to an object of the type
125     * 
126     * @param value Value to cast
127     * @param type Type to cast value in
128     * @return An object of the type 'type' with value 'value', or null if type
129     *         is unknown or value cannot be cast
130     */
131    public static Object castValue(String value, ParameterType type)
132    {
133        
134        if (value == null)
135        {
136            return null;
137        }
138
139        try
140        {
141            if (type == ParameterType.BOOLEAN)
142            {
143                return new Boolean(value);
144            }
145            else if (type == ParameterType.STRING)
146            {
147                return value;
148            }
149            else if (type == ParameterType.PASSWORD)
150            {
151                return value;
152            }
153            else if (type == ParameterType.DATASOURCE)
154            {
155                return value;
156            }
157            else if (type == ParameterType.LONG)
158            {
159                return new Long(value);
160            }
161            else if (type == ParameterType.DOUBLE)
162            {
163                return new Double(value);
164            }
165            else if (type == ParameterType.DATE)
166            {
167                LocalDateTime ldt = LocalDateTime.parse(value, DateTimeFormatter.ISO_DATE_TIME);
168                return DateUtils.asDate(ldt);
169            }
170            else if (type == ParameterType.BINARY)
171            {
172                return null;
173            }
174        }
175        catch (Exception nfe)
176        {
177            if (value.length() != 0)
178            {
179                _logger.error("Cannot cast value '" + value + "' into type '" + typeToString(type) + "'. Null object will be used.", nfe);
180            }
181            else if (_logger.isDebugEnabled())
182            {
183                _logger.debug("Failed to cast empty string to type '" + typeToString(type) + "'. Null object will be used.", nfe);
184            }
185        }
186        return null;
187    }
188    
189
190    /**
191     * Converts known types to string
192     * 
193     * @param value Typed value
194     * @return String readable by the config bean
195     * @throws IllegalArgumentException if the object is a InputStream
196     */
197    public static String valueToString(Object value)
198    {
199        if (value == null)
200        {
201            return null;
202        }
203
204        if (value instanceof Date)
205        {
206            ZonedDateTime zdt = DateUtils.asZonedDateTime((Date) value, null);           
207            return zdt.format(getISODateTimeFormatter());
208        }
209        
210        if (value instanceof InputStream)
211        {
212            throw new IllegalArgumentException("The object to convert is an input stream");
213        }
214
215        return value.toString();
216    }
217    
218    /**
219     * SAX a parameter
220     * @param handler The content handler where to SAX
221     * @param parameter The parameter to SAX
222     * @param value The parameter value. Can be null.
223     * @throws SAXException If an error occurred while SAXing
224     * @throws ProcessingException If an error occurred
225     */
226    public static void toSAXParameter (ContentHandler handler, Parameter parameter, Object value) throws SAXException, ProcessingException
227    {
228        AttributesImpl parameterAttr = new AttributesImpl();
229        parameterAttr.addAttribute("", "plugin", "plugin", "CDATA", parameter.getPluginName());
230        XMLUtils.startElement(handler, parameter.getId(), parameterAttr);
231        
232        toSAXParameterInternal(handler, parameter, value);
233        
234        XMLUtils.endElement(handler, parameter.getId());
235    }
236
237    /**
238     * SAX a parameter except the root tag
239     * @param handler The content handler where to SAX
240     * @param parameter The parameter to SAX
241     * @param value The parameter value. Can be null.
242     * @throws SAXException If an error occurred while SAXing
243     * @throws ProcessingException If an error occurred
244     */
245    public static void toSAXParameterInternal(ContentHandler handler, Parameter parameter, Object value) throws SAXException, ProcessingException
246    {
247        parameter.getLabel().toSAX(handler, "label");
248        parameter.getDescription().toSAX(handler, "description");
249        
250        XMLUtils.createElement(handler, "type", ParameterHelper.typeToString((ParameterType) parameter.getType()));
251        
252        Object defaultValue = parameter.getDefaultValue();
253        
254        if (defaultValue != null)
255        {
256            XMLUtils.createElement(handler, "default-value", ParameterHelper.valueToString(defaultValue));
257        }
258        
259        if (value != null)
260        {
261            XMLUtils.createElement(handler, "value", ParameterHelper.valueToString(value));
262        }
263        
264        if (parameter.getWidget() != null)
265        {
266            XMLUtils.createElement(handler, "widget", parameter.getWidget());
267        }
268        
269        Map<String, I18nizableText> widgetParameters = parameter.getWidgetParameters();
270        if (widgetParameters.size() > 0)
271        {
272            XMLUtils.startElement(handler, "widget-params");
273            for (String paramName : widgetParameters.keySet())
274            {
275                XMLUtils.startElement(handler, paramName);
276                widgetParameters.get(paramName).toSAX(handler);
277                XMLUtils.endElement(handler, paramName);
278            }
279            XMLUtils.endElement(handler, "widget-params");
280        }
281        
282        Enumerator enumerator = parameter.getEnumerator();
283        if (enumerator != null)
284        {
285            toSAXEnumerator(handler, enumerator);
286        }
287        
288        Validator validator = parameter.getValidator();
289        toSAXValidator(handler, validator);
290    }
291    
292    /**
293     * SAX parameter enumerator
294     * @param handler The content handler where to SAX
295     * @param enumerator The enumerator to SAX
296     * @throws SAXException If an error occurred to SAX
297     * @throws ProcessingException If an error occurred
298     */
299    public static void toSAXEnumerator (ContentHandler handler, Enumerator enumerator) throws SAXException, ProcessingException
300    {
301        XMLUtils.startElement(handler, "enumeration");
302        
303        try
304        {
305            for (Map.Entry<Object, I18nizableText> entry : enumerator.getEntries().entrySet())
306            {
307                String valueAsString = ParameterHelper.valueToString(entry.getKey());
308                I18nizableText label = entry.getValue();
309
310                // Generate option
311                AttributesImpl attrs = new AttributesImpl();
312                attrs.addCDATAAttribute("value", valueAsString);
313                
314                XMLUtils.startElement(handler, "option", attrs);
315                
316                if (label != null)
317                {
318                    label.toSAX(handler);
319                }
320                else
321                {
322                    XMLUtils.data(handler, valueAsString);
323                }
324                
325                XMLUtils.endElement(handler, "option");
326            }
327        }
328        catch (Exception e)
329        {
330            throw new ProcessingException("Unable to enumerate entries with enumerator: " + enumerator, e);
331        }
332
333        XMLUtils.endElement(handler, "enumeration");
334    }
335    
336    /**
337     * SAX parameter validator
338     * @param handler The content handler where to SAX
339     * @param validator The validator to SAX
340     * @throws SAXException If an error occurred while SAXing
341     */
342    public static void toSAXValidator (ContentHandler handler, Validator validator) throws SAXException
343    {
344        if (validator != null)
345        {
346            XMLUtils.startElement(handler, "validation");
347            
348            Map<String, Object> configuration = validator.getConfiguration();
349            
350            for (Map.Entry<String, Object> entry : configuration.entrySet())
351            {
352                _saxConfigurationObject(handler, entry.getKey(), entry.getValue());
353            }
354            
355            XMLUtils.endElement(handler, "validation");
356        }
357    }
358    
359    @SuppressWarnings("unchecked")
360    private static void _saxConfigurationObject(ContentHandler handler, String name, Object value) throws SAXException
361    {
362        if (value instanceof I18nizableText)
363        {
364            ((I18nizableText) value).toSAX(handler, name);
365        }
366        else if (value instanceof Collection)
367        {
368            for (Object item : (Collection) value)
369            {
370                if (item != null)
371                {
372                    _saxConfigurationObject(handler, name, item);
373                }
374            }
375        }
376        else if (value instanceof Map)
377        {
378            XMLUtils.startElement(handler, name);
379            for (Map.Entry<String, Object> subEntry : ((Map<String, Object>) value).entrySet())
380            {
381                _saxConfigurationObject(handler, subEntry.getKey(), subEntry.getValue());
382            }
383            XMLUtils.endElement(handler, name);
384        }
385        else if (value instanceof Object[])
386        {
387            for (Object item : (Object[]) value)
388            {
389                if (item != null)
390                {
391                    _saxConfigurationObject(handler, name, item);
392                }
393            }
394        }
395        else
396        {
397            XMLUtils.createElement(handler, name, String.valueOf(value));
398        }
399    }
400    
401    /**
402     * Convert the parameter in a JSON map
403     * @param parameter The parameter to convert
404     * @return The Parameter as a map
405     * @throws ProcessingException If an error occured when converting the parameter
406     */
407    public static Map<String, Object> toJSON(Parameter parameter) throws ProcessingException
408    {
409        Map<String, Object> result = new HashMap<>();
410        
411        result.put("id", parameter.getId());
412        result.put("label", parameter.getLabel());
413        result.put("description", parameter.getDescription());
414        result.put("plugin", parameter.getPluginName());
415        result.put("type", parameter.getType().name());
416        
417        if (parameter.getDefaultValue() != null)
418        {
419            result.put("default-value", parameter.getDefaultValue());
420        }
421        
422        if (parameter.getValidator() != null)
423        {
424            result.put("validation", parameter.getValidator().getConfiguration());
425        }
426        
427        if (parameter.getEnumerator() != null)
428        {
429            Enumerator enumerator = parameter.getEnumerator();
430            List<Map<String, Object>> enumeration = new ArrayList<>();
431            
432            try
433            {
434                Map<Object, I18nizableText> entries = enumerator.getEntries();
435                for (Object entryKey : entries.keySet())
436                {
437                    Map<String, Object> option = new HashMap<>();
438                    option.put("value", ParameterHelper.valueToString(entryKey));
439                    option.put("label", entries.get(entryKey));
440                    enumeration.add(option);
441                }
442            }
443            catch (Exception e)
444            {
445                throw new ProcessingException("Unable to enumerate entries with enumerator: " + enumerator, e);
446            }
447            
448            result.put("enumeration", enumeration);
449            result.put("enumerationConfig", enumerator.getConfiguration());
450        }
451        
452        if (parameter.getWidget() != null)
453        {
454            result.put("widget", parameter.getWidget());
455        }
456        
457        Map widgetParameters = parameter.getWidgetParameters();
458        if (widgetParameters != null && !widgetParameters.isEmpty())
459        {
460            result.put("widget-params", parameter.getWidgetParameters());
461        }
462        
463        return result;
464    }
465}