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