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