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.plugins.contentio;
017
018import java.io.ByteArrayInputStream;
019import java.io.IOException;
020import java.io.InputStream;
021import java.net.HttpURLConnection;
022import java.net.URL;
023import java.net.URLDecoder;
024import java.util.ArrayList;
025import java.util.Collection;
026import java.util.Collections;
027import java.util.Date;
028import java.util.HashMap;
029import java.util.HashSet;
030import java.util.Iterator;
031import java.util.List;
032import java.util.Map;
033import java.util.Set;
034import java.util.regex.Matcher;
035import java.util.regex.Pattern;
036
037import org.apache.avalon.framework.configuration.Configurable;
038import org.apache.avalon.framework.configuration.Configuration;
039import org.apache.avalon.framework.configuration.ConfigurationException;
040import org.apache.avalon.framework.logger.AbstractLogEnabled;
041import org.apache.avalon.framework.service.ServiceException;
042import org.apache.avalon.framework.service.ServiceManager;
043import org.apache.avalon.framework.service.Serviceable;
044import org.apache.commons.io.FilenameUtils;
045import org.apache.commons.io.IOUtils;
046import org.apache.commons.io.output.ByteArrayOutputStream;
047import org.apache.commons.lang3.StringUtils;
048import org.joda.time.format.ISODateTimeFormat;
049
050import org.ametys.cms.FilterNameHelper;
051import org.ametys.cms.contenttype.MetadataDefinition;
052import org.ametys.cms.repository.Content;
053import org.ametys.cms.repository.ContentQueryHelper;
054import org.ametys.cms.repository.WorkflowAwareContent;
055import org.ametys.cms.workflow.AbstractContentWorkflowComponent;
056import org.ametys.cms.workflow.ContentWorkflowHelper;
057import org.ametys.cms.workflow.EditContentFunction;
058import org.ametys.plugins.repository.AmetysObjectIterable;
059import org.ametys.plugins.repository.AmetysObjectResolver;
060import org.ametys.plugins.repository.metadata.ModifiableBinaryMetadata;
061import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
062import org.ametys.plugins.repository.metadata.ModifiableRichText;
063import org.ametys.plugins.repository.query.expression.AndExpression;
064import org.ametys.plugins.repository.query.expression.Expression;
065import org.ametys.plugins.repository.query.expression.Expression.Operator;
066import org.ametys.plugins.repository.query.expression.StringExpression;
067import org.ametys.plugins.workflow.AbstractWorkflowComponent;
068import org.ametys.runtime.parameter.ParameterHelper;
069import org.ametys.runtime.parameter.ParameterHelper.ParameterType;
070
071import com.opensymphony.workflow.WorkflowException;
072
073/**
074 * Abstract {@link ContentImporter} class which provides base importer configuration and logic.<br>
075 * Configuration options:
076 * <ul>
077 *   <li>Importer priority</li>
078 *   <li>Allowed extensions, without leading dot and comma-separated</li>
079 *   <li>Content types and mixins of the created contents</li>
080 *   <li>Language of the created contents</li>
081 *   <li>Content workflow name and creation action ID</li>
082 * </ul><br>
083 * Example configuration handled by the configure method:
084 * <pre>
085 * <extension point="org.ametys.plugins.contentio.ContentImporterExtensionPoint"
086 *               id="my.content.importer"
087 *               class="...">
088 *     <priority>500</priority>
089 *     <extensions>ext,ext2</extensions>
090 *     <content-creation>
091 *         <content-types>My.ContentType.1,My.ContentType.2</content-types>
092 *         <mixins>My.Mixin.1,My.Mixin.2</mixins>
093 *         <language>en</language>
094 *         <workflow name="content" createActionId="1" editActionId="2"/>
095 *     </content-creation>
096 * </extension>
097 * </pre>
098 */
099public abstract class AbstractContentImporter extends AbstractLogEnabled implements ContentImporter, Serviceable, Configurable
100{
101    
102    /** The default importer priority. */
103    protected static final int DEFAULT_PRIORITY = 5000;
104    
105    /** Map used to store the mapping from "local" ID to content ID, when actually imported. */
106    protected static final String _CONTENT_ID_MAP_KEY = AbstractContentImporter.class.getName() + "$contentIdMap";
107    
108    /** Map used to store the content references, indexed by content and metadata path. */
109    protected static final String _CONTENT_LINK_MAP_KEY = AbstractContentImporter.class.getName() + "$contentLinkMap";
110    
111    /** Map used to store the content repeater sizes. */
112    protected static final String _CONTENT_REPEATER_SIZE_MAP = AbstractContentImporter.class.getName() + "$contentRepeaterSizeMap";
113    
114    /** The AmetysObject resolver. */
115    protected AmetysObjectResolver _resolver;
116    
117    /** The content workflow helper. */
118    protected ContentWorkflowHelper _contentWorkflowHelper;
119    
120    /** The importer priority. */
121    protected int _priority = DEFAULT_PRIORITY;
122    
123    /** The allowed extensions. */
124    protected Set<String> _extensions;
125    
126    /** The imported contents' types. */
127    protected String[] _contentTypes;
128    
129    /** The imported contents' mixins. */
130    protected String[] _mixins;
131    
132    /** The importer contents' language. */
133    protected String _language;
134    
135    /** The importer contents' workflow name. */
136    protected String _workflowName;
137    
138    /** The importer contents' initial action ID. */
139    protected int _initialActionId;
140    
141    /** The importer contents' edition action ID. */
142    protected int _editActionId;
143    
144    @Override
145    public void service(ServiceManager manager) throws ServiceException
146    {
147        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
148        _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE);
149    }
150    
151    @Override
152    public void configure(Configuration configuration) throws ConfigurationException
153    {
154        _priority = configuration.getChild("priority").getValueAsInteger(DEFAULT_PRIORITY);
155        
156        configureExtensions(configuration.getChild("extensions"));
157        
158        configureContentCreation(configuration.getChild("content-creation"));
159    }
160    
161    /**
162     * Configure the allowed extensions.
163     * @param configuration the extension configuration.
164     * @throws ConfigurationException if an error occurs.
165     */
166    protected void configureExtensions(Configuration configuration) throws ConfigurationException
167    {
168        _extensions = new HashSet<>();
169        
170        String extensionsStr = configuration.getValue("");
171        
172        if (StringUtils.isBlank(extensionsStr))
173        {
174            _extensions.addAll(getDefaultExtensions());
175        }
176        else
177        {
178            for (String ext : StringUtils.split(extensionsStr, ", "))
179            {
180                String extension = ext.trim();
181                if (extension.startsWith("."))
182                {
183                    extension = extension.substring(1);
184                }
185                
186                _extensions.add(extension);
187            }
188        }
189    }
190    
191    /**
192     * Configure the content creation parameters.
193     * @param configuration the content creation configuration.
194     * @throws ConfigurationException if an error occurs.
195     */
196    protected void configureContentCreation(Configuration configuration) throws ConfigurationException
197    {
198        String typesStr = configuration.getChild("content-types").getValue();
199        _contentTypes = StringUtils.split(typesStr, ", ");
200        
201        String mixins = configuration.getChild("mixins").getValue("");  // mixins can be empty
202        _mixins = StringUtils.split(mixins, ", ");
203        
204        _language = configuration.getChild("language").getValue();
205        
206        configureWorkflow(configuration);
207    }
208    
209    /**
210     * Configure the content workflow.
211     * @param configuration the content creation configuration.
212     * @throws ConfigurationException if an error occurs.
213     */
214    protected void configureWorkflow(Configuration configuration) throws ConfigurationException
215    {
216        Configuration wfConf = configuration.getChild("workflow");
217        
218        _workflowName = wfConf.getAttribute("name");
219        
220        _initialActionId = wfConf.getAttributeAsInteger("createActionId");
221        _editActionId = wfConf.getAttributeAsInteger("editActionId");
222    }
223    
224    @Override
225    public int getPriority()
226    {
227        return _priority;
228    }
229    
230    /**
231     * Get the default allowed extensions.
232     * @return the default allowed extensions, without leading dots. Cannot be null.
233     */
234    protected Collection<String> getDefaultExtensions()
235    {
236        return Collections.emptySet();
237    }
238    
239    /**
240     * Test if the given filename has a supported extension.
241     * @param name the name, can't be null.
242     * @return true if the extension is supported, false otherwise.
243     * @throws IOException if an error occurs.
244     */
245    protected boolean isExtensionValid(String name) throws IOException
246    {
247        return _extensions.isEmpty() || _extensions.contains(FilenameUtils.getExtension(name));
248    }
249    
250    /**
251     * The content types of a created content.
252     * @param params the import parameters.
253     * @return the content types of a created content.
254     */
255    protected String[] getContentTypes(Map<String, Object> params)
256    {
257        return _contentTypes;
258    }
259    
260    /**
261     * The mixins of a created content.
262     * @param params the import parameters.
263     * @return The mixins of a created content.
264     */
265    protected String[] getMixins(Map<String, Object> params)
266    {
267        return _mixins;
268    }
269    
270    /**
271     * The language of a created content.
272     * @param params the import parameters.
273     * @return The language of a created content.
274     */
275    protected String getLanguage(Map<String, Object> params)
276    {
277        return _language;
278    }
279    
280    /**
281     * The workflow name of a created content.
282     * @param params the import parameters.
283     * @return The workflow name of a created content.
284     */
285    protected String getWorkflowName(Map<String, Object> params)
286    {
287        return _workflowName;
288    }
289    
290    /**
291     * The workflow creation action ID of a created content.
292     * @param params the import parameters.
293     * @return The workflow creation action ID of a created content.
294     */
295    protected int getInitialActionId(Map<String, Object> params)
296    {
297        return _initialActionId;
298    }
299    
300    /**
301     * The workflow action ID used to edit a content.
302     * @param params the import parameters.
303     * @return The workflow action ID used to edit a content.
304     */
305    protected int getEditActionId(Map<String, Object> params)
306    {
307        return _editActionId;
308    }
309    
310    /**
311     * Get the map used to store the mapping from "local" ID (defined in the import file)
312     * to the AmetysObject ID of the contents, when actually imported.
313     * @param params the import parameters.
314     * @return the content "local to repository" ID map.
315     */
316    protected Map<String, String> getContentIdMap(Map<String, Object> params)
317    {
318        // Get or create the map in the global parameters.
319        @SuppressWarnings("unchecked")
320        Map<String, String> contentIdMap = (Map<String, String>) params.get(_CONTENT_ID_MAP_KEY);
321        if (contentIdMap == null)
322        {
323            contentIdMap = new HashMap<>();
324            params.put(_CONTENT_ID_MAP_KEY, contentIdMap);
325        }
326        
327        return contentIdMap;
328    }
329    
330    /**
331     * Get the map used to store the content references.
332     * The Map is shaped like: referencing content -&gt; local metadata path -&gt; content references.
333     * @param params the import parameters.
334     * @return the content reference map.
335     */
336    protected Map<Content, Map<String, Object>> getContentRefMap(Map<String, Object> params)
337    {
338        // Get or create the map in the global parameters.
339        @SuppressWarnings("unchecked")
340        Map<Content, Map<String, Object>> contentRefMap = (Map<Content, Map<String, Object>>) params.get(_CONTENT_LINK_MAP_KEY);
341        if (contentRefMap == null)
342        {
343            contentRefMap = new HashMap<>();
344            params.put(_CONTENT_LINK_MAP_KEY, contentRefMap);
345        }
346        
347        return contentRefMap;
348    }
349    
350    /**
351     * Add a content reference to the map.
352     * @param content The referencing content.
353     * @param metadataPath The path of the metadata which holds the content references.
354     * @param reference The content reference.
355     * @param params The import parameters.
356     */
357    protected void addContentReference(Content content, String metadataPath, ContentReference reference, Map<String, Object> params)
358    {
359        addContentReference(getContentRefMap(params), content, metadataPath, reference);
360    }
361    
362    /**
363     * Add a content reference to the map.
364     * @param contentRefMap The content reference map.
365     * @param content The referencing content.
366     * @param metadataPath The path of the metadata which holds the content references.
367     * @param reference The content reference.
368     */
369    protected void addContentReference(Map<Content, Map<String, Object>> contentRefMap, Content content, String metadataPath, ContentReference reference)
370    {
371        Map<String, Object> contentReferences;
372        if (contentRefMap.containsKey(content))
373        {
374            contentReferences = contentRefMap.get(content);
375        }
376        else
377        {
378            contentReferences = new HashMap<>();
379            contentRefMap.put(content, contentReferences);
380        }
381        
382        contentReferences.put(metadataPath, reference);
383    }
384    
385    /**
386     * Add content references to the map.
387     * @param contentRefMap The content reference map.
388     * @param content The referencing content.
389     * @param metadataPath The path of the metadata which holds the content references.
390     * @param references the content reference list.
391     */
392    protected void addContentReferences(Map<Content, Map<String, Object>> contentRefMap, Content content, String metadataPath, List<ContentReference> references)
393    {
394        Map<String, Object> contentReferences;
395        if (contentRefMap.containsKey(content))
396        {
397            contentReferences = contentRefMap.get(content);
398        }
399        else
400        {
401            contentReferences = new HashMap<>();
402            contentRefMap.put(content, contentReferences);
403        }
404        
405        contentReferences.put(metadataPath, references);
406    }
407    
408    /**
409     * Get the map used to store the repeater sizes.
410     * The Map is shaped like: referencing content -&gt; local metadata path -&gt; content references.
411     * @param params the import parameters.
412     * @return the content reference map.
413     */
414    protected Map<Content, Map<String, Integer>> getContentRepeaterSizeMap(Map<String, Object> params)
415    {
416        // Get or create the map in the global parameters.
417        @SuppressWarnings("unchecked")
418        Map<Content, Map<String, Integer>> contentRepeaterSizeMap = (Map<Content, Map<String, Integer>>) params.get(_CONTENT_REPEATER_SIZE_MAP);
419        if (contentRepeaterSizeMap == null)
420        {
421            contentRepeaterSizeMap = new HashMap<>();
422            params.put(_CONTENT_REPEATER_SIZE_MAP, contentRepeaterSizeMap);
423        }
424        
425        return contentRepeaterSizeMap;
426    }
427    
428    /**
429     * Set a repeater size in the map (needed to execute the edit content function).
430     * @param content The content containing the repeater.
431     * @param metadataPath The repeater metadata path.
432     * @param repeaterSize The repeater size.
433     * @param params The import parameters.
434     */
435    protected void setRepeaterSize(Content content, String metadataPath, int repeaterSize, Map<String, Object> params)
436    {
437        Map<Content, Map<String, Integer>> contentRepeaterSizeMap = getContentRepeaterSizeMap(params);
438        
439        Map<String, Integer> repeaters;
440        if (contentRepeaterSizeMap.containsKey(content))
441        {
442            repeaters = contentRepeaterSizeMap.get(content);
443        }
444        else
445        {
446            repeaters = new HashMap<>();
447            contentRepeaterSizeMap.put(content, repeaters);
448        }
449        
450        repeaters.put(metadataPath, repeaterSize);
451    }
452    
453    /**
454     * Create a content.
455     * @param title the content title.
456     * @param params the import parameters.
457     * @return the created content.
458     * @throws WorkflowException if an error occurs.
459     */
460    protected Content createContent(String title, Map<String, Object> params) throws WorkflowException
461    {
462        String[] contentTypes = getContentTypes(params);
463        String[] mixins = getMixins(params);
464        String language = getLanguage(params);
465        String workflowName = getWorkflowName(params);
466        int initialActionId = getInitialActionId(params);
467        
468        return createContent(title, contentTypes, mixins, language, workflowName, initialActionId, params);
469    }
470    
471    /**
472     * Create a content.
473     * @param title the content title.
474     * @param contentTypes the content types.
475     * @param mixins the content mixins.
476     * @param language the content language.
477     * @param params the import parameters.
478     * @return the created content.
479     * @throws WorkflowException if an error occurs.
480     */
481    protected Content createContent(String title, String[] contentTypes, String[] mixins, String language, Map<String, Object> params) throws WorkflowException
482    {
483        String workflowName = getWorkflowName(params);
484        int initialActionId = getInitialActionId(params);
485        
486        return createContent(title, contentTypes, mixins, language, workflowName, initialActionId, params);
487    }
488    
489    /**
490     * Create a content.
491     * @param title the content title.
492     * @param contentTypes the content types.
493     * @param mixins the content mixins.
494     * @param language the content language.
495     * @param parentContentId the parent content ID.
496     * @param parentContentMetadataPath the parent content metadata path.
497     * @param params the import parameters.
498     * @return the created content.
499     * @throws WorkflowException if an error occurs.
500     */
501    protected Content createContent(String title, String[] contentTypes, String[] mixins, String language, String parentContentId, String parentContentMetadataPath, Map<String, Object> params) throws WorkflowException
502    {
503        String workflowName = getWorkflowName(params);
504        int initialActionId = getInitialActionId(params);
505        
506        return createContent(title, contentTypes, mixins, language, workflowName, initialActionId, parentContentId, parentContentMetadataPath, params);
507    }
508    
509    /**
510     * Create a content.
511     * @param title the content title.
512     * @param contentTypes the content types.
513     * @param mixins the content mixins.
514     * @param language the content language.
515     * @param workflowName the content workflow name.
516     * @param initialActionId the content create action ID.
517     * @param params the import parameters.
518     * @return the created content.
519     * @throws WorkflowException if an error occurs.
520     */
521    protected Content createContent(String title, String[] contentTypes, String[] mixins, String language, String workflowName, int initialActionId, Map<String, Object> params) throws WorkflowException
522    {
523        return createContent(title, contentTypes, mixins, language, workflowName, initialActionId, null, null, params);
524    }
525    
526    /**
527     * Create a content.
528     * @param title the content title.
529     * @param contentTypes the content types.
530     * @param mixins the content mixins.
531     * @param language the content language.
532     * @param workflowName the content workflow name.
533     * @param initialActionId the content create action ID.
534     * @param parentContentId the parent content ID.
535     * @param parentContentMetadataPath the parent content metadata path.
536     * @param params the import parameters.
537     * @return the created content.
538     * @throws WorkflowException if an error occurs.
539     */
540    protected Content createContent(String title, String[] contentTypes, String[] mixins, String language, String workflowName, int initialActionId, String parentContentId, String parentContentMetadataPath, Map<String, Object> params) throws WorkflowException
541    {
542        String name;
543        try
544        {
545            name = FilterNameHelper.filterName(title);
546        }
547        catch (Exception e)
548        {
549            // Ignore the exception, just provide a valid start.
550            name = "content-" + title;
551        }
552        
553        Map<String, Object> result = _contentWorkflowHelper.createContent(workflowName, initialActionId, name, title, contentTypes, mixins, language, parentContentId, parentContentMetadataPath);
554        
555        return (Content) result.get(AbstractContentWorkflowComponent.CONTENT_KEY);
556    }
557    
558    /**
559     * Set a string metadata.
560     * @param meta the metadata holder.
561     * @param name the metadata name.
562     * @param metaDef the metadata definition.
563     * @param values the metadata values.
564     */
565    protected void setStringMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String[] values)
566    {
567        if (values != null)
568        {
569            if (metaDef.isMultiple())
570            {
571                meta.setMetadata(name, values);
572            }
573            else
574            {
575                meta.setMetadata(name, values[0]);
576            }
577        }
578    }
579    
580    /**
581     * Set a boolean metadata.
582     * @param meta the metadata holder.
583     * @param name the metadata name.
584     * @param metaDef the metadata definition.
585     * @param values the metadata values.
586     */
587    protected void setBooleanMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String[] values)
588    {
589        if (values != null)
590        {
591            if (metaDef.isMultiple())
592            {
593                boolean[] bValues = new boolean[values.length];
594                for (int i = 0; i < values.length; i++)
595                {
596                    bValues[i] = Boolean.parseBoolean(values[i]);
597                }
598                
599                meta.setMetadata(name, bValues);
600            }
601            else
602            {
603                meta.setMetadata(name, Boolean.parseBoolean(values[0]));
604            }
605        }
606    }
607    
608    /**
609     * Set a long metadata.
610     * @param meta the metadata holder.
611     * @param name the metadata name.
612     * @param metaDef the metadata definition.
613     * @param values the metadata values.
614     */
615    protected void setLongMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String[] values)
616    {
617        if (values != null)
618        {
619            if (metaDef.isMultiple())
620            {
621                long[] lValues = new long[values.length];
622                for (int i = 0; i < values.length; i++)
623                {
624                    lValues[i] = Long.parseLong(values[i]);
625                }
626                
627                meta.setMetadata(name, lValues);
628            }
629            else
630            {
631                meta.setMetadata(name, Long.parseLong(values[0]));
632            }
633        }
634    }
635    
636    /**
637     * Set a double metadata.
638     * @param meta the metadata holder.
639     * @param name the metadata name.
640     * @param metaDef the metadata definition.
641     * @param values the metadata values.
642     */
643    protected void setDoubleMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String[] values)
644    {
645        if (values != null)
646        {
647            if (metaDef.isMultiple())
648            {
649                double[] dValues = new double[values.length];
650                for (int i = 0; i < values.length; i++)
651                {
652                    dValues[i] = Double.parseDouble(values[i]);
653                }
654                
655                meta.setMetadata(name, dValues);
656            }
657            else
658            {
659                meta.setMetadata(name, Double.parseDouble(values[0]));
660            }
661        }
662    }
663    
664    /**
665     * Set a date or datetime metadata.
666     * @param meta the metadata holder.
667     * @param name the metadata name.
668     * @param metaDef the metadata definition.
669     * @param values the metadata values.
670     */
671    protected void setDateMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String[] values)
672    {
673        if (values != null)
674        {
675            if (metaDef.isMultiple())
676            {
677                Date[] dValues = new Date[values.length];
678                for (int i = 0; i < values.length; i++)
679                {
680                    dValues[i] = parseDate(values[i]);
681                }
682                
683                meta.setMetadata(name, dValues);
684            }
685            else
686            {
687                meta.setMetadata(name, parseDate(values[0]));
688            }
689        }
690    }
691    
692    /**
693     * Parse a String value as a Date.<br>
694     * Allowed formats:
695     * <ul>
696     *   <li>yyyy-MM-dd</li>
697     *   <li>yyyy-MM-dd'T'HH:mm:ss.SSSZZ</li>
698     * </ul>
699     * @param value the String value.
700     * @return the parsed Date or <code>null</code> if the value can't be parsed.
701     */
702    protected Date parseDate(String value)
703    {
704        return parseDate(value, false);
705    }
706    
707    /**
708     * Parse a String value as a Date.<br>
709     * Allowed formats:
710     * <ul>
711     *   <li>yyyy-MM-dd</li>
712     *   <li>yyyy-MM-dd'T'HH:mm:ss.SSSZZ</li>
713     * </ul>
714     * @param value the String value.
715     * @param throwException true to throw an exception if the value can't be parsed, false to return null.
716     * @return the parsed Date or <code>null</code> if the value can't be parsed and throwException is false.
717     */
718    protected Date parseDate(String value, boolean throwException)
719    {
720        Date dateValue = null;
721        
722        try
723        {
724            dateValue = ISODateTimeFormat.date().parseDateTime(value).toDate();
725        }
726        catch (Exception e)
727        {
728            dateValue = (Date) ParameterHelper.castValue(value, ParameterType.DATE);
729        }
730        
731        if (dateValue == null && throwException)
732        {
733            throw new IllegalArgumentException("'" + value + "' could not be cast as a Date.");
734        }
735        
736        return dateValue;
737    }
738    
739    /**
740     * Set a geocode metadata.
741     * @param meta the metadata holder.
742     * @param name the metadata name.
743     * @param metaDef the metadata definition.
744     * @param latitude the geocode latitude as a String.
745     * @param longitude the geocode longitude as a String.
746     */
747    protected void setGeocodeMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String latitude, String longitude)
748    {
749        if (StringUtils.isNotEmpty(latitude) && StringUtils.isNotEmpty(longitude))
750        {
751            double dLat = Double.parseDouble(latitude);
752            double dLong = Double.parseDouble(longitude);
753            
754            setGeocodeMetadata(meta, name, metaDef, dLat, dLong);
755        }
756        else
757        {
758            throw new IllegalArgumentException("Invalid geocode values: latitude='" + latitude + "', longitude='" + longitude + "'.");
759        }
760    }
761    
762    /**
763     * Set a geocode metadata.
764     * @param meta the metadata holder.
765     * @param name the metadata name.
766     * @param metaDef the metadata definition.
767     * @param latitude the geocode latitude.
768     * @param longitude the geocode longitude.
769     */
770    protected void setGeocodeMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, double latitude, double longitude)
771    {
772        ModifiableCompositeMetadata geoCode = meta.getCompositeMetadata(name, true);
773        geoCode.setMetadata("longitude", longitude);
774        geoCode.setMetadata("latitude", latitude);
775    }
776    
777    /**
778     * Set a file metadata.
779     * @param meta the metadata holder.
780     * @param name the metadata name.
781     * @param metaDef the metadata definition
782     * @param value the value
783     * @throws IOException if an exception occurs when manipulating files
784     */
785    protected void setBinaryMetadata(ModifiableCompositeMetadata meta, String name, MetadataDefinition metaDef, String value) throws IOException
786    {
787        if (StringUtils.isNotEmpty(value))
788        {
789            try
790            {
791                Pattern pattern = Pattern.compile("filename=\"([^\"]+)\"");
792                
793                URL url = new URL(value);
794                HttpURLConnection connection = (HttpURLConnection) url.openConnection();
795                connection.setConnectTimeout(1000);
796                connection.setReadTimeout(2000);
797             
798                try (InputStream is = connection.getInputStream())
799                {
800                    String contentType = StringUtils.defaultString(connection.getContentType(), "application/unknown");
801                    String contentEncoding = StringUtils.defaultString(connection.getContentEncoding(), "");
802                    String contentDisposition = StringUtils.defaultString(connection.getHeaderField("Content-Disposition"), "");
803                    String filename = URLDecoder.decode(FilenameUtils.getName(connection.getURL().getPath()), "UTF-8");
804                    if (StringUtils.isEmpty(filename))
805                    {
806                        Matcher matcher = pattern.matcher(contentDisposition);
807                        if (matcher.matches())
808                        {
809                            filename = matcher.group(1);
810                        }
811                        else
812                        {
813                            filename = "unknown";
814                        }
815                    }
816                    
817                    try (ByteArrayOutputStream bos = new ByteArrayOutputStream())
818                    {
819                        IOUtils.copy(is, bos);
820                        
821                        ModifiableBinaryMetadata binaryMeta = meta.getBinaryMetadata(name, true);
822                        binaryMeta.setLastModified(new Date());
823                        binaryMeta.setInputStream(new ByteArrayInputStream(bos.toByteArray()));
824                        if (StringUtils.isNotEmpty(filename))
825                        {
826                            binaryMeta.setFilename(filename);
827                        }
828                        if (StringUtils.isNotEmpty(contentType))
829                        {
830                            binaryMeta.setMimeType(contentType);
831                        }
832                        if (StringUtils.isNotEmpty(contentEncoding))
833                        {
834                            binaryMeta.setEncoding(contentEncoding);
835                        }
836                    }
837                }
838            }
839            catch (Exception e)
840            {
841                throw new IllegalArgumentException("Unable to fetch file from URL '" + value + "', it will be ignored.", e);
842            }
843        }
844    }
845    
846    /**
847     * Set a RichText metadata.
848     * @param meta the metadata holder.
849     * @param name the metadata name.
850     * @param data an input stream on the rich text content.
851     */
852    protected void setRichText(ModifiableCompositeMetadata meta, String name, InputStream data)
853    {
854        ModifiableRichText richText = meta.getRichText(name, true);
855        
856        richText.setEncoding("UTF-8");
857        richText.setLastModified(new Date());
858        richText.setMimeType("text/xml");
859        richText.setInputStream(data);
860    }
861    
862    /**
863     * Restore content references.
864     * @param params The import parameters.
865     */
866    protected void restoreContentReferences(Map<String, Object> params)
867    {
868        Map<Content, Map<String, Object>> contentRefMap = getContentRefMap(params);
869        Map<Content, Map<String, Integer>> contentRepeaterSizeMap = getContentRepeaterSizeMap(params);
870        int editActionId = getEditActionId(params);
871        
872        for (Content content : contentRefMap.keySet())
873        {
874            if (content instanceof WorkflowAwareContent)
875            {
876                Map<String, Object> contentReferences = contentRefMap.get(content);
877                Map<String, Integer> repeaters = contentRepeaterSizeMap.get(content);
878                
879                Map<String, Object> values = new HashMap<>();
880                
881                // Fill the value map with the content references.
882                setReferenceMetadatas(contentReferences, values, repeaters, params);
883                
884                try
885                {
886                    if (!values.isEmpty())
887                    {
888                        Map<String, Object> contextParameters = new HashMap<>();
889                        contextParameters.put(EditContentFunction.QUIT, true);
890                        contextParameters.put(EditContentFunction.FORM_RAW_VALUES, values);
891                        
892                        Map<String, Object> inputs = new HashMap<>();
893                        inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, contextParameters);
894                        inputs.put(AbstractContentWorkflowComponent.CONTENT_KEY, content);
895                        
896                        _contentWorkflowHelper.doAction((WorkflowAwareContent) content, editActionId, inputs);
897                    }
898                }
899                catch (WorkflowException e)
900                {
901                    // TODO Throw exception?
902                    getLogger().warn("An error occurred restoring content references in content " + content, e);
903                }
904            }
905        }
906    }
907    
908    /**
909     * Fill the value map with the content references.
910     * @param contentReferences The list of content references indexed by metadata path.
911     * @param values The value map passed to the EditContentFunction class.
912     * @param repeaters The repeater sizes for this content.
913     * @param params The import parameters.
914     */
915    protected void setReferenceMetadatas(Map<String, Object> contentReferences, Map<String, Object> values, Map<String, Integer> repeaters, Map<String, Object> params)
916    {
917        for (String metadataPath : contentReferences.keySet())
918        {
919            Object value = contentReferences.get(metadataPath);
920            String metaKey = EditContentFunction.FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
921            
922            if (value instanceof List<?>)
923            {
924                // Multiple value
925                @SuppressWarnings("unchecked")
926                List<ContentReference> references = (List<ContentReference>) value;
927                List<String> contentIds = new ArrayList<>(references.size());
928                for (ContentReference reference : references)
929                {
930                    String refContentId = getReferencedContentId(reference, params);
931                    if (refContentId != null)
932                    {
933                        contentIds.add(refContentId);
934                    }
935                }
936                
937                if (!contentIds.isEmpty())
938                {
939                    values.put(metaKey, contentIds);
940                }
941            }
942            else if (value instanceof ContentReference)
943            {
944                // Single value.
945                String refContentId = getReferencedContentId((ContentReference) value, params);
946                if (refContentId != null)
947                {
948                    values.put(metaKey, refContentId);
949                }
950            }
951        }
952        
953        if (repeaters != null)
954        {
955            for (String repeaterPath : repeaters.keySet())
956            {
957                Integer size = repeaters.get(repeaterPath);
958                if (size > 0)
959                {
960                    String sizeKey = EditContentFunction.INTERNAL_FORM_ELEMENTS_PREFIX + repeaterPath.replace('/', '.') + ".size";
961                    values.put(sizeKey, repeaters.get(repeaterPath).toString());
962                }
963            }
964        }
965    }
966    
967    /**
968     * Get the content ID from a content reference.
969     * @param contentRef The content reference.
970     * @param params The import parameters.
971     * @return the content ID if it was found, or null otherwise.
972     */
973    protected String getReferencedContentId(ContentReference contentRef, Map<String, Object> params)
974    {
975        int refType = contentRef.getType();
976        if (refType == ContentReference.TYPE_LOCAL_ID)
977        {
978            String localId = (String) contentRef.getValue();
979            String contentId = getContentIdMap(params).get(localId);
980            if (StringUtils.isNotEmpty(contentId) && _resolver.hasAmetysObjectForId(contentId))
981            {
982                return contentId;
983            }
984        }
985        else if (refType == ContentReference.TYPE_CONTENT_ID)
986        {
987            String contentId = (String) contentRef.getValue();
988            if (StringUtils.isNotEmpty(contentId) && _resolver.hasAmetysObjectForId(contentId))
989            {
990                return contentId;
991            }
992        }
993        else if (refType == ContentReference.TYPE_CONTENT_VALUES)
994        {
995            @SuppressWarnings("unchecked")
996            Map<String, String> values = (Map<String, String>) contentRef.getValue();
997            Content content = getContentFromProperties(values);
998            if (content != null)
999            {
1000                return content.getId();
1001            }
1002        }
1003        
1004        return null;
1005    }
1006    
1007    /**
1008     * Search a content from a map of its metadata values.
1009     * @param propertyValues The metadata values.
1010     * @return The Content if found, null otherwise.
1011     */
1012    protected Content getContentFromProperties(Map<String, String> propertyValues)
1013    {
1014        Content content = null;
1015        
1016        List<Expression> expressions = new ArrayList<>();
1017        for (String property : propertyValues.keySet())
1018        {
1019            String value = propertyValues.get(property);
1020            expressions.add(new StringExpression(property, Operator.EQ, value)); 
1021        }
1022        
1023        Expression[] exprArray = expressions.toArray(new Expression[expressions.size()]);
1024        
1025        String query = ContentQueryHelper.getContentXPathQuery(new AndExpression(exprArray));
1026        
1027        AmetysObjectIterable<Content> contents = _resolver.query(query);
1028        Iterator<Content> it = contents.iterator();
1029        
1030        if (it.hasNext())
1031        {
1032            content = it.next();
1033            
1034            if (it.hasNext())
1035            {
1036                content = null;
1037            }
1038        }
1039        
1040        return content;
1041    }
1042    
1043    /**
1044     * Class representing a reference to a content in an import file.
1045     */
1046    public class ContentReference
1047    {
1048        
1049        /**
1050         * The referenced content doesn't exist in the repository, it's in the import file.
1051         * The reference value is the content ID in the import file.
1052         */
1053        public static final int TYPE_LOCAL_ID = 1;
1054        
1055        /**
1056         * The referenced content exits in the repository and its ID is known.
1057         * The reference value is the content ID in the repository (AmetysObject ID).
1058         */
1059        public static final int TYPE_CONTENT_ID = 2;
1060        
1061        /**
1062         * The referenced content exits in the repository. Its ID is not known,
1063         * but it can be identified by one or several of its metadata.
1064         * The reference value is a Map of metadata name -&gt; value.
1065         */
1066        public static final int TYPE_CONTENT_VALUES = 3;
1067        
1068        /** The reference type. */
1069        private int _type;
1070        
1071        /** The reference value, depends on the reference type. */
1072        private Object _value;
1073        
1074        /**
1075         * Build a content reference.
1076         * @param type the reference type.
1077         * @param value the reference value.
1078         */
1079        public ContentReference(int type, Object value)
1080        {
1081            this._type = type;
1082            this._value = value;
1083        }
1084
1085        /**
1086         * Get the type.
1087         * @return the type
1088         */
1089        public int getType()
1090        {
1091            return _type;
1092        }
1093
1094        /**
1095         * Set the type.
1096         * @param type the type to set
1097         */
1098        public void setType(int type)
1099        {
1100            this._type = type;
1101        }
1102
1103        /**
1104         * Get the value.
1105         * @return the value
1106         */
1107        public Object getValue()
1108        {
1109            return _value;
1110        }
1111
1112        /**
1113         * Set the value.
1114         * @param value the value to set
1115         */
1116        public void setValue(Object value)
1117        {
1118            this._value = value;
1119        }
1120        
1121    }
1122    
1123}