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.cms.workflow;
017
018import java.io.ByteArrayInputStream;
019import java.io.IOException;
020import java.io.UnsupportedEncodingException;
021import java.lang.reflect.Array;
022import java.time.LocalDate;
023import java.time.LocalDateTime;
024import java.time.format.DateTimeFormatter;
025import java.time.format.DateTimeParseException;
026import java.util.ArrayList;
027import java.util.Arrays;
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.LinkedHashSet;
035import java.util.List;
036import java.util.Map;
037import java.util.Map.Entry;
038import java.util.NoSuchElementException;
039import java.util.Set;
040import java.util.TreeMap;
041
042import javax.jcr.Node;
043import javax.jcr.RepositoryException;
044import javax.jcr.lock.Lock;
045import javax.jcr.lock.LockManager;
046
047import org.apache.avalon.framework.activity.Initializable;
048import org.apache.commons.collections.CollectionUtils;
049import org.apache.commons.collections.comparators.ReverseComparator;
050import org.apache.commons.lang3.ArrayUtils;
051import org.apache.commons.lang3.LocaleUtils;
052import org.apache.commons.lang3.StringUtils;
053import org.apache.commons.lang3.math.NumberUtils;
054
055import org.ametys.cms.ObservationConstants;
056import org.ametys.cms.content.external.ExternalizableMetadataHelper;
057import org.ametys.cms.content.external.ExternalizableMetadataProvider.ExternalizableMetadataStatus;
058import org.ametys.cms.content.external.ExternalizableMetadataProviderExtensionPoint;
059import org.ametys.cms.content.references.OutgoingReferences;
060import org.ametys.cms.content.references.OutgoingReferencesExtractor;
061import org.ametys.cms.contenttype.AbstractMetadataSetElement;
062import org.ametys.cms.contenttype.ContentConstants;
063import org.ametys.cms.contenttype.ContentType;
064import org.ametys.cms.contenttype.ContentTypeExtensionPoint;
065import org.ametys.cms.contenttype.ContentTypesHelper;
066import org.ametys.cms.contenttype.ContentValidator;
067import org.ametys.cms.contenttype.MetadataDefinition;
068import org.ametys.cms.contenttype.MetadataDefinitionReference;
069import org.ametys.cms.contenttype.MetadataSet;
070import org.ametys.cms.contenttype.MetadataType;
071import org.ametys.cms.contenttype.RepeaterDefinition;
072import org.ametys.cms.form.AbstractField;
073import org.ametys.cms.form.AbstractField.MODE;
074import org.ametys.cms.form.BinaryField;
075import org.ametys.cms.form.ExternalizableField;
076import org.ametys.cms.form.Form;
077import org.ametys.cms.form.ReferenceField;
078import org.ametys.cms.form.RepeaterField;
079import org.ametys.cms.form.RepeaterField.RepeaterEntry;
080import org.ametys.cms.form.RichTextField;
081import org.ametys.cms.form.SimpleField;
082import org.ametys.cms.form.SubContentField;
083import org.ametys.cms.repository.Content;
084import org.ametys.cms.repository.ModifiableContent;
085import org.ametys.cms.repository.WorkflowAwareContent;
086import org.ametys.cms.transformation.RichTextTransformer;
087import org.ametys.core.observation.Event;
088import org.ametys.core.observation.ObservationManager;
089import org.ametys.core.upload.Upload;
090import org.ametys.core.upload.UploadManager;
091import org.ametys.core.user.User;
092import org.ametys.core.user.UserIdentity;
093import org.ametys.core.user.UserManager;
094import org.ametys.core.util.DateUtils;
095import org.ametys.core.util.JSONUtils;
096import org.ametys.plugins.core.user.UserHelper;
097import org.ametys.plugins.repository.AmetysObject;
098import org.ametys.plugins.repository.AmetysObjectIterable;
099import org.ametys.plugins.repository.AmetysObjectResolver;
100import org.ametys.plugins.repository.AmetysRepositoryException;
101import org.ametys.plugins.repository.RepositoryConstants;
102import org.ametys.plugins.repository.TraversableAmetysObject;
103import org.ametys.plugins.repository.UnknownAmetysObjectException;
104import org.ametys.plugins.repository.lock.LockHelper;
105import org.ametys.plugins.repository.lock.LockableAmetysObject;
106import org.ametys.plugins.repository.metadata.BinaryMetadata;
107import org.ametys.plugins.repository.metadata.CommentableCompositeMetadata;
108import org.ametys.plugins.repository.metadata.CompositeMetadata;
109import org.ametys.plugins.repository.metadata.MetadataComment;
110import org.ametys.plugins.repository.metadata.ModifiableBinaryMetadata;
111import org.ametys.plugins.repository.metadata.ModifiableCompositeMetadata;
112import org.ametys.plugins.repository.metadata.ModifiableFolder;
113import org.ametys.plugins.repository.metadata.ModifiableResource;
114import org.ametys.plugins.repository.metadata.ModifiableRichText;
115import org.ametys.plugins.repository.metadata.MultilingualString;
116import org.ametys.plugins.repository.metadata.Resource;
117import org.ametys.plugins.repository.metadata.UnknownMetadataException;
118import org.ametys.plugins.repository.metadata.jcr.JCRCompositeMetadata;
119import org.ametys.plugins.workflow.AbstractWorkflowComponent;
120import org.ametys.plugins.workflow.component.CheckRightsCondition;
121import org.ametys.plugins.workflow.support.WorkflowProvider;
122import org.ametys.plugins.workflow.support.WorkflowProvider.AmetysObjectWorkflow;
123import org.ametys.runtime.config.Config;
124import org.ametys.runtime.i18n.I18nizableText;
125import org.ametys.runtime.parameter.Errors;
126import org.ametys.runtime.parameter.Validator;
127
128import com.opensymphony.module.propertyset.PropertySet;
129import com.opensymphony.workflow.FunctionProvider;
130import com.opensymphony.workflow.WorkflowException;
131
132/**
133 * OSWorkflow function to edit a content.
134 * 
135 * The required transient variables:
136 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY - Map<String, Object> The map containing the results of the function.
137 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.result - String "true" when everything goes fine. Missing in other case.
138 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.<MetadataPath> - Errors Each error during edition will be set here. Key will be the metadata path (with '.' separator). Value will be the error message.
139 * - AbstractContentWorkflowComponent.CONTENT_KEY - WorkflowAwareContent The content that will be edited. Should have the lock token.
140 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY - Map<String, Object> Contains the following parameters:
141 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY.QUIT - boolean True to specify edition mode will be quit, this imply to unlock the content.
142 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY.METADATA_SET_PARAM The name of the edition metadata set to use and to check metadata. If missing a metadataset will be created.
143 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY.FORM_RAW_VALUES - Map<String, Object> The values of the submitted form :
144 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY.FORM_RAW_VALUES.<MetadataPath> Object Key is the path of the metadata ('.' separated) prefixed by FORM_ELEMENTS_PREFIX. Value is a depending on the type of metadata.
145 *                                                                                         Sometimes types require additional information. In that case : Key is a metadata path ('.' separated) prefixed by INTERNAL_FORM_ELEMENTS_PREFIX and suffixed by '.' + an additional information name.
146 * - AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY.FORM_RAW_COMMENTS - Map<String, List<Map<String, String>>> The comments of the metadata of the submitted form :
147 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.<MetadataPath> - List<Map<String, String>> Key is the path of the metadata ('.' separated) prefixed by FORM_ELEMENTS_PREFIX. Value is the list of comments.
148 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.<MetadataPath>.<X> - <Map<String, String> A comment with the following parameters
149 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.<MetadataPath>.<X>.author String The login of the author of the comment
150 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.<MetadataPath>.<X>.text String The text of the comment
151 * - AbstractContentWorkflowComponent.RESULT_MAP_KEY.<MetadataPath>.<X>.date String The date of the comment using the ISODateTimeFormat (See DateUtils.parse)
152 * 
153 * Where <MetadataPath> is the path of a metadata (using a '.' separator). In some cases it is prefixed by FORM_ELEMENTS_PREFIX. A metadata path with in a repeater include the number of the repeated instance (1 based).
154 * Where <X> Is an element of the parent list.
155 * 
156 * Here is the list of required information and values depending on the type of the metadata:
157 * - MetadataType.STRING
158 *      When simple, the value is a String containing the value. Ex: "content.input.abstract": "Sample of abstract"
159 *      When multiple, the value is a String[] containing the values. Ex: "content.input.abstract": ["value 1", "value 2"]. An additional information can be provided: "mode" that can be "replace" (default value) or "insert". When "replace", the given String[] will replace the existing value. When "insert"; the given String[] will be appended. Ex: "_content.input.abstract.mode": "insert".
160 * - MetadataType.DATE 
161 *      Same as MetadataType.STRING where values are String encoded at ISODateTimeFormat (See DateUtils.parse). Ex: "content.input.date": "2014-03-12T00:00:00.000+01:00".
162 * - MetadataType.DATETIME
163 *      See MetadataType.DATE.
164 * - MetadataType.LONG
165 *      Same as MetadataType.STRING where values are String that can be parsed as Long.
166 * - MetadataType.DOUBLE
167 *      Same as MetadataType.STRING where values are String that can be parsed as Double.
168 * - MetadataType.BOOLEAN
169 *      Same as MetadataType.STRING where values are String that can be parsed as Boolean.
170 * - MetadataType.GEOCODE
171 *      A single String that is are a map encoded with 2 keys 'latitude' and 'longitude' and double values. Ex: "content.input.address.gps": "{\"latitude\":1.574576,\"longitude\":103.79255799999999}"
172 * - MetadataType.COMPOSITE
173 *      A composite metadata does not have values itself. The values are sub metadata.
174 *      A repeater (sort of multiple composite) does not have value either, but do have additional information: 
175 *          "size" in a String representing a Long with the number of elements submitted. Ex: "_content.input.attachments.size": "2". This information is crucial since this function only handle repeated values going from 1 to size. Ex: "content.input.attachments.1.*" and "content.input.attachments.2.*".
176 *          "mode" as for MetadataType.STRING, mode can be "replace" (default value) to remove all current entries before adding new one, or "insert" to add only the new values (see "position" under to see where to insert).
177 *          Each instance in the repeater does also have additional information.
178 *              "previous-position" As explained above, a metadatapath inside a repeater include the current position of the element. This value is a String encoding a Long with the position of the repeater instance BEFORE this edition: this allow to move the repeater instance, instead of removing it and add it again (specially with FILE or BINARY metadata). Ex: "_content.input.attachments.1.previous-position": "1". Will be "-1" when it is a new instance.
179 *              "position" In "insert" mode, we do not want to use the value set in the metadata path as a "current position". The position given by path is wrong as it always start at 1 and finish at size. Values >= 1 are positions. Ex: "_content.input.attachments.1.position": "20", means that all values "content.input.attachments.1.*" will be added to an instance at the position "20" (and not "1" as it will be in "replace" mode). Values < 1 are positions indexed by the end. 0 means to add it to the end. -1 to add it just before the last one. In "insert" mode, elements are insersected at the given positions and do not replace those elements. Inserting 5 elements at "position" "1", will insert 5 elements at position 1, 2, 3, 4 and 5.
180 * - MetadataType.BINARY
181 *      TODO
182 * - MetadataType.RICH_TEXT
183 *      TODO
184 * - MetadataType.USER
185 *      A single String that is a map encoded with 2 keys 'login' and 'populationId' and string values. Ex: "content.input.user": "{\"login\":"alogin",\"populationId\":"apopulation"}"
186 * - MetadataType.REFERENCE
187 *      When simple, a single String that is a map encoded with 2 keys 'value' and 'type', both are string. 'type' is the type of the reference (ex: external-url), and 'value' its value.
188 *      When multiple, an Array of String. Where each string is formatted the same way as done in the simple case.
189 * - MetadataType.CONTENT
190 *      TODO
191 * - MetadataType.SUB_CONTENT
192 *      TODO
193 * - MetadataType.FILE
194 *      TODO
195 *      TODO talk about UNTOUCHED_BINARY UNTOUCHED_FILE METADATA_FILE EXPLORER_FILE
196 */
197public class EditContentFunction extends AbstractContentWorkflowComponent implements FunctionProvider, Initializable
198{
199    /** Constant for storing the action id for editing revert relations. */
200    public static final String INVERT_RELATION_EDIT_WORKFLOW_ACTION_ID = EditContentFunction.class.getName() + "$invertEditActionId";
201    
202    /** Constant for storing the action id for editing revert relations. */
203    public static final String EDIT_MUTUAL_RELATIONSHIP = EditContentFunction.class.getName() + "$mutualRelationship";
204    
205    /** Prefix for HTML form elements. */
206    public static final String FORM_ELEMENTS_PREFIX = "content.input.";
207    /** The key for global errors */
208    public static final String GLOBAL_ERROR_KEY = "_global";
209    /** Prefix for internal HTML form elements. */
210    public static final String INTERNAL_FORM_ELEMENTS_PREFIX = "_" + FORM_ELEMENTS_PREFIX;
211    /** Request parameter key for the field values. */
212    public static final String FORM_RAW_VALUES = "values";
213    /** Request parameter key for the field comments. */
214    public static final String FORM_RAW_COMMENTS = "comments";
215    /** Prefix for the metadata set name request parameter. */
216    public static final String METADATA_SET_PARAM = "content.metadata.set";
217    /** Prefix for the fallback metadata set name request parameter. */
218    public static final String FALLBACK_METADATA_SET_PARAM = "content.fallback.metadata.set";
219    /** Prefix for the quit edition mode request parameter. */
220    public static final String QUIT = "quit";
221    /** Constant for untouched binary metadata. */
222    public static final String UNTOUCHED_BINARY = "untouched";
223    /** Constant for untouched file metadata. */
224    public static final String UNTOUCHED_FILE = "untouched";
225    /** Constant for local file metadata. */
226    public static final String METADATA_FILE = "metadata";
227    /** Constant for shared file metadata. */
228    public static final String EXPLORER_FILE = "explorer";
229    /** Default action id of editing revert relations. */
230    public static final int INVERT_EDIT_ACTION_ID = 2;
231    
232    /** The AmetysObject resolver. */
233    protected AmetysObjectResolver _resolver;
234    /** Content type extension point. */
235    protected ContentTypeExtensionPoint _contentTypeExtensionPoint;
236    /** Helper for content types */
237    protected ContentTypesHelper _contentTypesHelper;
238    /** Upload manager. */
239    protected UploadManager _uploadManager;
240    /** Observation manager available to subclasses. */
241    protected ObservationManager _observationManager;
242    /** The JSON conversion utilities. */
243    protected JSONUtils _jsonUtils;
244    /** The workflow provider */
245    protected WorkflowProvider _workflowProvider;
246    /** The content workflow helper. */
247    protected ContentWorkflowHelper _workflowHelper;
248    /** The outgoing references extractor */
249    protected OutgoingReferencesExtractor _outgoingReferencesExtractor;
250    /** The user manager */
251    protected UserManager _userManager;
252    /** The user helper */
253    protected UserHelper _userHelper;    
254    /** Provider for externalizable metadata */
255    protected ExternalizableMetadataProviderExtensionPoint _externalizableMetadataProviderEP;
256    /** Set of already checked node */
257    protected Set<String> _lockAlreadyChecked = new HashSet<>();
258    
259    @Override
260    public void initialize() throws Exception
261    {
262        _resolver = (AmetysObjectResolver) _manager.lookup(AmetysObjectResolver.ROLE);
263        _contentTypeExtensionPoint = (ContentTypeExtensionPoint) _manager.lookup(ContentTypeExtensionPoint.ROLE);
264        _uploadManager = (UploadManager) _manager.lookup(UploadManager.ROLE);
265        _observationManager = (ObservationManager) _manager.lookup(ObservationManager.ROLE);
266        _jsonUtils = (JSONUtils) _manager.lookup(JSONUtils.ROLE);
267        _workflowProvider = (WorkflowProvider) _manager.lookup(WorkflowProvider.ROLE);
268        _workflowHelper = (ContentWorkflowHelper) _manager.lookup(ContentWorkflowHelper.ROLE);
269        _contentTypesHelper = (ContentTypesHelper) _manager.lookup(ContentTypesHelper.ROLE);
270        _outgoingReferencesExtractor = (OutgoingReferencesExtractor) _manager.lookup(OutgoingReferencesExtractor.ROLE);
271        _userManager = (UserManager) _manager.lookup(UserManager.ROLE);
272        _userHelper = (UserHelper) _manager.lookup(UserHelper.ROLE);
273        _externalizableMetadataProviderEP = (ExternalizableMetadataProviderExtensionPoint) _manager.lookup(ExternalizableMetadataProviderExtensionPoint.ROLE);
274    }
275    
276    @SuppressWarnings("unchecked")
277    @Override
278    public void execute(Map transientVars, Map args, PropertySet ps) throws WorkflowException
279    {
280        _logger.info("Performing edit workflow function");
281
282        _lockAlreadyChecked = new HashSet<>();
283        
284        // Retrieve current content
285        WorkflowAwareContent content = getContent(transientVars);
286        UserIdentity user = getUser(transientVars);
287        
288        // Get the action id for editing invert relations
289        int invertEditActionId = _getInvertEditActionId(transientVars);
290        
291        if (!(content instanceof ModifiableContent))
292        {
293            throw new IllegalArgumentException("The provided content " + content.getId() + " is not a ModifiableContent.");
294        }
295        
296        ModifiableContent modifiableContent = (ModifiableContent) content;
297        
298        try
299        {
300            LockableAmetysObject lockableContent = null;
301            if (content instanceof LockableAmetysObject)
302            {
303                lockableContent = (LockableAmetysObject) content;
304                if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, user))
305                {
306                    throw new WorkflowException("User '" + user + "' try to save content '" + modifiableContent.getName() + "' but it is locked by another user");
307                }
308            }
309            
310            AllErrors errors = new AllErrors();
311            
312            Map<String, Object> parameters = getContextParameters(transientVars);  
313            
314            long time_0 = System.currentTimeMillis();
315            
316            Map<String, Object> rawValues = (Map<String, Object>) parameters.get(FORM_RAW_VALUES);
317            if (rawValues == null)
318            {
319                rawValues = Collections.EMPTY_MAP;
320            }
321            
322            MetadataSet metadataSet = getMetadataSet(parameters, rawValues, modifiableContent);
323            
324            long time_1 = System.currentTimeMillis();
325            
326            Map<String, List<Map<String, String>>> rawComments = (Map<String, List<Map<String, String>>>) parameters.get(FORM_RAW_COMMENTS);
327            
328            Set<String> externalAndLocalMetadata = _externalizableMetadataProviderEP.getExternalAndLocalMetadata(modifiableContent);
329            
330            _bindAndValidateContent(modifiableContent, errors, metadataSet, rawValues, rawComments, user, invertEditActionId, externalAndLocalMetadata);
331
332            long time_2 = System.currentTimeMillis();
333            
334            _handleErrors(transientVars, modifiableContent, errors);
335            
336            _updateCommonMetadata(modifiableContent, user);
337            
338            _extractOutgoingReferences(modifiableContent);
339            
340            long time_3 = System.currentTimeMillis();
341            
342            // Commit changes
343            modifiableContent.saveChanges();
344            
345            long time_4 = System.currentTimeMillis();
346            
347            // Notify the observers of the modification.
348            _notifyContentModified(content, transientVars);
349            
350            long time_5 = System.currentTimeMillis();
351            
352            // Unlock content if we are not in save & quit mode
353            Boolean quit = (Boolean) parameters.get(QUIT);
354            if (quit != null && quit && lockableContent != null && lockableContent.isLocked())
355            {
356                lockableContent.unlock();
357            }
358            
359            long time_6 = System.currentTimeMillis();
360            
361            boolean logAbnormalTime = Config.getInstance().getValue("runtime.log.abnormal.time");
362            if (time_6 - time_0 > 5000 && logAbnormalTime)
363            {
364                _logger.warn("Edit content action has taken an abnormally long time : get metadata set in " + (time_1 - time_0) + " ms / bind metadata in " + (time_2 - time_1) + " ms / build consistencies in " + (time_3 - time_2) + " ms / save in " + (time_4 - time_3) + " / notify listeners in " + (time_5 - time_4) + " / total in " + (time_6 - time_0) + " ms");
365            }
366            else if (_logger.isDebugEnabled())
367            {
368                _logger.debug("Edit timers : get metadata set in " + (time_1 - time_0) + " ms / bind metadata in " + (time_2 - time_1) + " ms / build consistencies in " + (time_3 - time_2) + " ms / save in " + (time_4 - time_3) + " / notify listeners in " + (time_5 - time_4) + " / total in " + (time_6 - time_0) + " ms");
369            }
370            
371            getResultsMap(transientVars).put("result", "ok");
372        }
373        catch (AmetysRepositoryException e)
374        {
375            throw new WorkflowException("Unable to edit content " + modifiableContent + " from the repository", e);
376        }
377    }
378
379    private void _handleErrors(Map transientVars, ModifiableContent modifiableContent, AllErrors errors) throws WorkflowException, InvalidInputWorkflowException
380    {
381        if (errors.hasErrors())
382        {
383            // Populate the map to render
384            Map<String, Object> result = getResultsMap(transientVars);
385            
386            Map<String, I18nizableText> errorFieldLabels = new HashMap<>();
387            
388            for (Map.Entry<String, Errors> entry : errors.getAllErrors().entrySet())
389            {
390                String canonicalMetadataPath = entry.getKey().replace('/', '.');
391                
392                result.put(canonicalMetadataPath, entry.getValue());
393                
394                MetadataDefinition metadataDefinition = _contentTypesHelper.getMetadataDefinition(entry.getKey(), modifiableContent);
395                if (metadataDefinition != null)
396                {
397                    errorFieldLabels.put(canonicalMetadataPath, metadataDefinition.getLabel());
398                }
399            }
400            
401            result.put("errorFieldLabels", errorFieldLabels);
402            
403            throw new InvalidInputWorkflowException("At least one validation error is preventing from saving the modifications", errors);
404        }
405    }
406    
407    /**
408     * Get the identifier of the invert edit action
409     * @param transientVars The workflow vars
410     * @return the identifier of the invert edit action
411     */
412    protected int _getInvertEditActionId(Map transientVars)
413    {
414        return transientVars.containsKey(INVERT_RELATION_EDIT_WORKFLOW_ACTION_ID) ? (Integer) transientVars.get(INVERT_RELATION_EDIT_WORKFLOW_ACTION_ID) : INVERT_EDIT_ACTION_ID;
415    }
416    
417    /**
418     * Notify observers that the content has been modified
419     * @param content The content modified
420     * @param transientVars The workflow vars
421     * @throws WorkflowException If an error occurred
422     */
423    protected void _notifyContentModified(Content content, Map transientVars) throws WorkflowException
424    {
425        Map<String, Object> eventParams = new HashMap<>();
426        eventParams.put(ObservationConstants.ARGS_CONTENT, content);
427        eventParams.put(ObservationConstants.ARGS_CONTENT_ID, content.getId());
428        
429        if (transientVars.containsKey(EditContentFunction.EDIT_MUTUAL_RELATIONSHIP))
430        {
431            eventParams.put(EditContentFunction.EDIT_MUTUAL_RELATIONSHIP, true);
432        }
433        _observationManager.notify(new Event(ObservationConstants.EVENT_CONTENT_MODIFIED, getUser(transientVars), eventParams));
434    }
435
436    /**
437     * Get the metadata set for the content
438     * @param jsParameters The request parameters
439     * @param rawValues The raw values of the form
440     * @param content The content
441     * @return The metadata set asked in the request or a built-in metadataset
442     * @throws WorkflowException If an error occurred while getting the metadata set
443     */
444    protected MetadataSet getMetadataSet(Map<String, Object> jsParameters, Map<String, Object> rawValues, Content content) throws WorkflowException
445    {
446        MetadataSet metadataSet;
447        
448        String metadataSetName = (String) jsParameters.get(METADATA_SET_PARAM);
449        String fallbackMetadataSetName = (String) jsParameters.get(FALLBACK_METADATA_SET_PARAM);
450        if (metadataSetName != null)
451        {
452            try
453            {
454                metadataSet = _contentTypesHelper.getMetadataSetWithFallback(metadataSetName, fallbackMetadataSetName, content.getTypes(), content.getMixinTypes(), true);
455            }
456            catch (IllegalArgumentException e)
457            {
458                throw new WorkflowException(e);
459            }
460        }
461        else
462        {
463            // Let us compute a metadataset
464            metadataSet = _createMetadataSet(rawValues);
465        }
466        
467        return metadataSet;
468    }
469
470    private MetadataSet _createMetadataSet(Map<String, Object> rawValues)
471    {
472        MetadataSet metadataSet;
473        metadataSet = new MetadataSet();
474        metadataSet.setName("__generated__");
475        metadataSet.setLabel(new I18nizableText("Live edition metadataset"));
476        metadataSet.setDescription(new I18nizableText("Live edition metadataset"));
477        metadataSet.setSmallIcon(null);
478        metadataSet.setMediumIcon(null);
479        metadataSet.setLargeIcon(null);
480        metadataSet.setEdition(true);
481        metadataSet.setInternal(true);
482         
483        // Lets remove numbers in repeaters definitions
484        @SuppressWarnings("unchecked")
485        Map<String, Integer> sizePrefixes = new TreeMap<>(new ReverseComparator());
486        for (String parameterName : rawValues.keySet())
487        {
488            if (StringUtils.startsWith(parameterName, INTERNAL_FORM_ELEMENTS_PREFIX) && StringUtils.endsWith(parameterName, ".size"))
489            {
490                String prefix = parameterName.substring(1, parameterName.length() - 5); // 5 is ".size" length
491                Integer size = Integer.parseInt((String) rawValues.get(parameterName));
492                
493                sizePrefixes.put(prefix, size);
494            }
495        }
496        
497        Set<String> parameterNames2MetadataDefPaths = new HashSet<>(rawValues.keySet());
498        boolean setWasModified = true;
499        while (setWasModified)
500        {
501            setWasModified = false;
502            
503            Iterator<String> sizePrefixesIterator = sizePrefixes.keySet().iterator();
504            while (!setWasModified && sizePrefixesIterator.hasNext())
505            {
506                String sizePrefix = sizePrefixesIterator.next();
507                
508                for (int i = 1; !setWasModified && i <= sizePrefixes.get(sizePrefix); i++)
509                {
510                    String numPrefix = "." + i + ".";
511                    String prefix = sizePrefix + numPrefix;
512                    
513                    Iterator<String> parameterNames2MetadataDefPathsIterator = parameterNames2MetadataDefPaths.iterator();
514                    while (!setWasModified && parameterNames2MetadataDefPathsIterator.hasNext())
515                    {
516                        String parameterNames2MetadataDefPath = parameterNames2MetadataDefPathsIterator.next();
517                        
518                        if (StringUtils.startsWith(parameterNames2MetadataDefPath, prefix))
519                        {
520                            parameterNames2MetadataDefPaths.remove(parameterNames2MetadataDefPath);
521                            
522                            String newName = parameterNames2MetadataDefPath.substring(0, prefix.length() - numPrefix.length()) + parameterNames2MetadataDefPath.substring(prefix.length() - 1);
523                            parameterNames2MetadataDefPaths.add(newName);
524                            
525                            setWasModified = true;
526                        }
527                    }
528                }
529            }
530        }
531        
532        for (String parameterName : parameterNames2MetadataDefPaths)
533        {
534            if (parameterName.startsWith(FORM_ELEMENTS_PREFIX))
535            {
536                String metadataName = parameterName.substring(FORM_ELEMENTS_PREFIX.length());
537                _addMetadataDefRef(metadataSet, metadataName);
538            }
539        }
540        return metadataSet;
541    }
542    
543    private void _addMetadataDefRef(AbstractMetadataSetElement metadataSetElement, String metadataName)
544    {
545        String currentLevelMetadataName = StringUtils.substringBefore(metadataName, ".");
546        MetadataDefinitionReference metaDefRef = metadataSetElement.getMetadataDefinitionReference(currentLevelMetadataName);
547        if (metaDefRef == null)
548        {
549            metaDefRef = new MetadataDefinitionReference(currentLevelMetadataName, "main");
550            metadataSetElement.addElement(metaDefRef);
551        }
552        
553        String subLevelMetadataName = StringUtils.substringAfter(metadataName, ".");
554        if (StringUtils.isNotBlank(subLevelMetadataName))
555        {
556            _addMetadataDefRef(metaDefRef, subLevelMetadataName);
557        }
558    }
559
560    /**
561     * Analyze the content to extract outgoing references and store them
562     * @param content The content to analyze
563     */
564    protected void _extractOutgoingReferences(ModifiableContent content)
565    {
566        Map<String, OutgoingReferences> outgoingReferencesByPath = _outgoingReferencesExtractor.getOutgoingReferences(content);
567        content.setOutgoingReferences(outgoingReferencesByPath);
568    }
569    
570    /**
571     * Template method to indicates if invert relation should be taken into account during the whole edition.
572     * Override and return false to disabled invert relation management.
573     * @return true if invert relation are enabled
574     */
575    protected boolean _invertRelationEnabled()
576    {
577        return true;
578    }
579    
580    /**
581     * Bind and validate a form.
582     * @param content the content.
583     * @param allErrors the errors.
584     * @param metadataSet the metadataset
585     * @param rawValues the raw values of the form
586     * @param rawComments the raw comments of the form
587     * @param user the user.
588     * @param invertEditActionId The current 'edit content' action ID.
589     * @param externalAndLocalMetadata The paths of externalizable metadata
590     * @throws WorkflowException if an error occurs.
591     */
592    protected void _bindAndValidateContent(ModifiableContent content, AllErrors allErrors, MetadataSet metadataSet, Map<String, Object> rawValues, Map<String, List<Map<String, String>>> rawComments, UserIdentity user, int invertEditActionId, Set<String> externalAndLocalMetadata) throws WorkflowException
593    {
594        Form form = new Form();
595        
596        // First bind and validate in the form object
597        _bindAndValidateMetadataSetElement(content, form, allErrors, metadataSet, rawValues, rawComments, null, "", externalAndLocalMetadata);
598        
599        // Additional validation
600        _validateForm(content, form, metadataSet, allErrors);
601        
602        // Do not synchronize if there is at least one error
603        if (!allErrors.hasErrors())
604        {
605            // Prepare to synchronize
606            _prepareSynchronizeMetadataSetElement(content, content.getMetadataHolder(), form, allErrors, user, metadataSet, null, "", invertEditActionId);
607            
608            if (!allErrors.hasErrors())
609            {
610                // Synchronize form fields and content metadata if no error
611                _synchronizeMetadataSetElement(content, content.getMetadataHolder(), form, allErrors, user, metadataSet, null, "", invertEditActionId, externalAndLocalMetadata);
612            }
613            
614        }
615    }
616
617    /**
618     * Bind and validate a metadata set element.
619     * @param content the content.
620     * @param form the form.
621     * @param allErrors the errors.
622     * @param metadataSetElement the metadata set element for this metadata.
623     * @param rawValues the raw values of the form
624     * @param rawComments the raw comments of the form
625     * @param parentMetadataDefinition the metadata definition.
626     * @param parentMetadataPath the metadata path.
627     * @param externalAndLocalMetadata The paths of externalizable metadata
628     * @throws WorkflowException if an error occurs.
629     */
630    protected void _bindAndValidateMetadataSetElement(Content content, Form form, AllErrors allErrors, AbstractMetadataSetElement metadataSetElement, Map<String, Object> rawValues, Map<String, List<Map<String, String>>> rawComments, MetadataDefinition parentMetadataDefinition, String parentMetadataPath, Set<String> externalAndLocalMetadata) throws WorkflowException
631    {
632        for (AbstractMetadataSetElement subElement : metadataSetElement.getElements())
633        {
634            if (subElement instanceof MetadataDefinitionReference)
635            {
636                MetadataDefinitionReference metadataDefRef = (MetadataDefinitionReference) subElement;
637                MetadataDefinition metadataDefinition = _getMetadataDefinition(content, parentMetadataDefinition, metadataDefRef.getMetadataName());
638                
639                if (metadataDefinition == null)
640                {
641                    throw new IllegalArgumentException("Unable to get the metadata definition of metadata \"" + parentMetadataPath + ContentConstants.METADATA_PATH_SEPARATOR + metadataDefRef.getMetadataName() + "\"");
642                }
643                
644                if (_contentTypesHelper.canWrite(content, metadataDefinition))
645                {
646                    String subMetadataPath = parentMetadataPath + metadataDefinition.getName();
647                    _bindAndValidateMetadata(content, form, allErrors, subElement, rawValues, rawComments, metadataDefinition, subMetadataPath, externalAndLocalMetadata);
648                    _bindComments(rawComments, form, metadataDefinition, subMetadataPath); // Handle metadata comments
649                }
650            }
651            else
652            {
653                _bindAndValidateMetadataSetElement(content, form, allErrors, subElement, rawValues, rawComments, parentMetadataDefinition, parentMetadataPath, externalAndLocalMetadata);
654            }
655        }
656    }
657
658    /**
659     * Validates the form.
660     * @param content the content.
661     * @param form the form.
662     * @param metadataSet the metadata set.
663     * @param allErrors the errors.
664     */
665    protected void _validateForm(Content content, Form form, MetadataSet metadataSet, AllErrors allErrors)
666    {
667        Errors errors = new Errors();
668        
669        String[] allContentTypes = ArrayUtils.addAll(content.getTypes(), content.getMixinTypes());
670        
671        for (String cTypeId : allContentTypes)
672        {
673            ContentType contentType = _contentTypeExtensionPoint.getExtension(cTypeId);
674            
675            for (ContentValidator validator : contentType.getGlobalValidators())
676            {
677                validator.validate(content, form, metadataSet, errors);
678            }
679        }
680        
681        if (errors.hasErrors())
682        {
683            // Global error
684            allErrors.addError(GLOBAL_ERROR_KEY, errors);
685        }
686       
687    }
688
689    /**
690     * Synchronize a metadata set element with a composite metadata.
691     * @param content the content.
692     * @param metadata the composite metadata to synchronize.
693     * @param form the form.
694     * @param allErrors the errors.
695     * @param user the user.
696     * @param metadataSetElement the metadata set element for this metadata.
697     * @param parentMetadataDefinition the metadata definition.
698     * @param metadataPath the metadata path.
699     * @param invertEditActionId The action id for editing invert relation
700     * @param externalAndLocalMetadata The paths of externalizable metadata
701     * @throws WorkflowException if an error occurs.
702     */
703    protected void _synchronizeMetadataSetElement(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, AbstractMetadataSetElement metadataSetElement, MetadataDefinition parentMetadataDefinition, String metadataPath, int invertEditActionId, Set<String> externalAndLocalMetadata) throws WorkflowException
704    {
705        for (AbstractMetadataSetElement subElement : metadataSetElement.getElements())
706        {
707            if (subElement instanceof MetadataDefinitionReference)
708            {
709                MetadataDefinitionReference metadataDefRef = (MetadataDefinitionReference) subElement;
710                MetadataDefinition metadataDefinition = _getMetadataDefinition(content, parentMetadataDefinition, metadataDefRef.getMetadataName());
711                
712                if (_contentTypesHelper.canWrite(content, metadataDefinition))
713                {
714                    String subMetadataPath = metadataPath + metadataDefinition.getName();
715                    _synchronizeMetadata(content, metadata, form, allErrors, user, subElement, metadataDefinition, subMetadataPath, invertEditActionId, externalAndLocalMetadata);
716                }
717            }
718            else
719            {
720                _synchronizeMetadataSetElement(content, metadata, form, allErrors, user, subElement, parentMetadataDefinition, metadataPath, invertEditActionId, externalAndLocalMetadata);
721            }
722        }
723    }
724    
725    /**
726     * Synchronize to synchronize a metadata set element with a composite metadata.
727     * @param content the content.
728     * @param metadata the composite metadata to synchronize.
729     * @param form the form.
730     * @param allErrors the errors.
731     * @param user the user.
732     * @param metadataSetElement the metadata set element for this metadata.
733     * @param parentMetadataDefinition the metadata definition.
734     * @param metadataPath the metadata path.
735     * @param invertEditActionId The action id for editing invert relation
736     * @throws WorkflowException if an error occurs.
737     * @throws AmetysRepositoryException if an error occurred
738     */
739    protected void _prepareSynchronizeMetadataSetElement(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, AbstractMetadataSetElement metadataSetElement, MetadataDefinition parentMetadataDefinition, String metadataPath, int invertEditActionId) throws WorkflowException, AmetysRepositoryException
740    {
741        for (AbstractMetadataSetElement subElement : metadataSetElement.getElements())
742        {
743            if (subElement instanceof MetadataDefinitionReference)
744            {
745                MetadataDefinitionReference metadataDefRef = (MetadataDefinitionReference) subElement;
746                MetadataDefinition metadataDefinition = _getMetadataDefinition(content, parentMetadataDefinition, metadataDefRef.getMetadataName());
747                
748                if (_contentTypesHelper.canWrite(content, metadataDefinition))
749                {
750                    String subMetadataPath = metadataPath + metadataDefinition.getName();
751                    _prepareSynchronizeMetadata(content, metadata, form, allErrors, user, subElement, metadataDefinition, subMetadataPath, invertEditActionId);
752                }
753            }
754            else
755            {
756                _prepareSynchronizeMetadataSetElement(content, metadata, form, allErrors, user, subElement, parentMetadataDefinition, metadataPath, invertEditActionId);
757            }
758        }
759    }
760    
761    
762
763    /**
764     * Updates common metadata (last contributor, last modification date, ...).
765     * @param content the content.
766     * @param user the user.
767     * @throws WorkflowException if an error occurs.
768     */
769    protected void _updateCommonMetadata(ModifiableContent content, UserIdentity user) throws WorkflowException
770    {
771        if (user != null)
772        {
773            content.setLastContributor(user);
774        }
775        content.setLastModified(new Date());
776        
777        if (content instanceof WorkflowAwareContent)
778        {
779            // Remove the proposal date.
780            ((WorkflowAwareContent) content).setProposalDate(null);
781        }
782    }
783
784    /**
785     * Retrieves a sub metadata definition from a content type or
786     * a parent metadata definition. 
787     * @param content the content.
788     * @param parentMetadataDefinition the parent metadata definition.
789     * @param metadataName the metadata name.
790     * @return the metadata definition found or <code>null</code> otherwise.
791     */
792    protected MetadataDefinition _getMetadataDefinition(Content content, MetadataDefinition parentMetadataDefinition, String metadataName)
793    {
794        if (parentMetadataDefinition == null)
795        {
796            return _contentTypesHelper.getMetadataDefinition(metadataName, content.getTypes(), content.getMixinTypes());
797        }
798        else
799        {
800            return parentMetadataDefinition.getMetadataDefinition(metadataName);
801        }
802    }
803    
804    /**
805     * Bind and validate a form.
806     * @param content the content.
807     * @param form the form.
808     * @param allErrors the errors.
809     * @param metadataSetElement the metadata set element for this metadata.
810     * @param rawValues the raw values.
811     * @param rawComments the raw comments.
812     * @param metadataDefinition the metadata definition.
813     * @param metadataPath the metadata path.
814     * @param externalAndLocalMetadata The paths of externalizable metadata
815     * @throws WorkflowException if an error occurs.
816     */
817    protected void _bindAndValidateMetadata(Content content, Form form, AllErrors allErrors, AbstractMetadataSetElement metadataSetElement, Map<String, Object> rawValues, Map<String, List<Map<String, String>>> rawComments, MetadataDefinition metadataDefinition, String metadataPath, Set<String> externalAndLocalMetadata) throws WorkflowException
818    {
819        String metadataName = metadataDefinition.getName();
820        MetadataType type = metadataDefinition.getType();
821        
822        if (!_contentTypesHelper.canWrite(content, metadataDefinition))
823        {
824            throw new WorkflowException("Current user has no right to edit metadata " + metadataName);
825        }
826        
827        boolean externalizable = externalAndLocalMetadata.contains(metadataPath);
828        Object rawValue = rawValues.get(FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.'));
829        
830        switch (type)
831        {
832            case STRING:
833                _bindAndValidateStringMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
834                break;
835            case MULTILINGUAL_STRING:
836                _bindAndValidateMultilingualStringMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
837                break;
838            case USER:
839                _bindAndValidateUserMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
840                break;
841            case DATE:
842                _bindAndValidateDateMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
843                break;
844            case DATETIME:
845                _bindAndValidateDateTimeMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
846                break;
847            case LONG:
848                _bindAndValidateLongMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
849                break;
850            case GEOCODE:
851                _bindAndValidateGeocodeMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
852                break;
853            case DOUBLE:
854                _bindAndValidateDoubleMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
855                break;
856            case BOOLEAN:
857                _bindAndValidateBooleanMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
858                break;
859            case BINARY:
860                _bindAndValidateBinaryMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
861                break;
862            case FILE:
863                _bindAndValidateFileMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
864                break;
865            case RICH_TEXT:
866                _bindAndValidateRichText(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
867                break;
868            case COMPOSITE:
869                _bindAndValidateCompositeMetadata(allErrors, form, content, metadataName, metadataSetElement, metadataDefinition, metadataPath, rawValue, rawValues, rawComments, externalAndLocalMetadata);
870                break;
871            case REFERENCE:
872                _bindAndValidateReferenceMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
873                break;
874            case CONTENT:
875                _bindAndValidateContentReferenceMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
876                break;
877            case SUB_CONTENT:
878                _bindAndValidateSubContentMetadata(allErrors, form, content, metadataDefinition, metadataPath, rawValue, rawValues, externalizable);
879                break;
880            default:
881                throw new WorkflowException("Unsupported type: " + type);   
882        }
883    }
884    
885    private String[] _getLocalValues (String rawValue, MetadataDefinition metadataDefinition)
886    {
887        Map<String, Object> externalizableValue = _jsonUtils.convertJsonToMap(rawValue);
888        return _getMetadataValues(externalizableValue.get("local"), metadataDefinition);
889    }
890    
891    private String[] _getExternalValues (String rawValue, MetadataDefinition metadataDefinition)
892    {
893        Map<String, Object> externalizableValue = _jsonUtils.convertJsonToMap(rawValue);
894        return _getMetadataValues(externalizableValue.get("external"), metadataDefinition);
895    }
896
897    /**
898     * Get a metadata values from the request.
899     * @param rawValues the raw values.
900     * @param form the form.
901     * @param metadataDefinition the metadata definition.
902     * @param metadataPath the metadata path.
903     * @return the metadata values as a String array.
904     */
905    protected String[] _getMetadataValues(Map<String, Object> rawValues, Form form, MetadataDefinition metadataDefinition, String metadataPath)
906    {
907        Object rawValue = rawValues.get(FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.'));
908        return _getMetadataValues(rawValue, metadataDefinition);
909    }
910    
911    /**
912     * Get a metadata values from raw value
913     * @param rawValue The raw value
914     * @param metadataDefinition the metadata definition.
915     * @return the metadata values as a String array.
916     */
917    @SuppressWarnings("unchecked")
918    protected String[] _getMetadataValues(Object rawValue, MetadataDefinition metadataDefinition)
919    {
920        List<String> metadataValues = new ArrayList<>();
921        
922        if (rawValue != null)
923        {
924            if (metadataDefinition.isMultiple())
925            {
926                // The value can either be passed as a List object or as a JSON-encoded string.
927                List<Object> listValue = Collections.emptyList();
928                if (rawValue instanceof List)
929                {
930                    listValue = (List<Object>) rawValue;
931                }
932                else if (rawValue instanceof String)
933                {
934                    listValue = _jsonUtils.convertJsonToList((String) rawValue);
935                }
936                
937                for (Object valueAsObject : listValue)
938                {
939                    if (valueAsObject instanceof String)
940                    {
941                        metadataValues.add((String) valueAsObject); 
942                    }
943                    else
944                    {
945                        metadataValues.add(_jsonUtils.convertObjectToJson(valueAsObject));
946                    }
947                }
948            }
949            else
950            {
951                metadataValues.add(String.valueOf(rawValue));
952            }
953        }
954        
955        return metadataValues.toArray(new String[metadataValues.size()]);
956    }
957    
958    /**
959     * Bind the comments of a field to the form
960     * @param rawComments The raw comments of the form
961     * @param form The forms
962     * @param metadataDefinition the metadata definition.
963     * @param metadataPath the metadata path.
964     */
965    protected void _bindComments(Map<String, List<Map<String, String>>> rawComments, Form form, MetadataDefinition metadataDefinition, String metadataPath)
966    {
967        if (rawComments == null)
968        {
969            return;
970        }
971
972        String fieldName = metadataDefinition.getName();
973        List<Map<String, String>> rawFieldcomments = rawComments.get(FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.'));
974        List<MetadataComment> comments = new ArrayList<>();
975        
976        if (rawFieldcomments != null)
977        {
978            for (Map<String, String> rawEntry : rawFieldcomments)
979            {
980                String text = rawEntry.get("text");
981                String author = rawEntry.get("author");
982                Date date = DateUtils.parse(rawEntry.get("date"));
983                comments.add(new MetadataComment(text, date, author));
984            }
985        }
986        
987        form.setCommentsField(fieldName, comments.toArray(new MetadataComment[comments.size()]));
988    }
989    
990    /**
991     * Bind and validate a composite metadata.
992     * @param allErrors for storing validation errors.
993     * @param form the form. 
994     * @param metadataDefinition the metadata definition.
995     * @param content the current content
996     * @param metadataName the metadata name
997     * @param metadataSetElement the metadata set element
998     * @param metadataPath the metadata path from the content.
999     * @param rawValue the submitted values.
1000     * @param rawValues The raw values of the form
1001     * @param rawComments The raw comments of the form
1002     * @param externalAndLocalMetadata The paths of externalizable metadata
1003     * @throws AmetysRepositoryException if an error occurs.
1004     * @throws WorkflowException if an error occurs.
1005     */
1006    protected void _bindAndValidateCompositeMetadata(AllErrors allErrors, Form form, Content content, String metadataName, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, Map<String, List<Map<String, String>>> rawComments, Set<String> externalAndLocalMetadata) throws AmetysRepositoryException, WorkflowException
1007    {
1008        if (metadataDefinition instanceof RepeaterDefinition)
1009        {
1010            _bindAndValidateRepeater(content, form, allErrors, metadataSetElement, rawValues, rawComments, (RepeaterDefinition) metadataDefinition, metadataPath, externalAndLocalMetadata);
1011        }
1012        else
1013        {
1014            String key = FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
1015            
1016            Form compositeForm = null;
1017            if (!rawValues.containsKey(key) || rawValues.get(key) != null)
1018            {
1019                compositeForm = new Form();
1020                _bindAndValidateMetadataSetElement(content, compositeForm, allErrors, metadataSetElement, rawValues, rawComments, metadataDefinition, metadataPath + "/", externalAndLocalMetadata);
1021            }
1022            form.setCompositeField(metadataName, compositeForm);
1023        }
1024    }
1025
1026    /**
1027     * Bind and validate a repeater.
1028     * @param content the content.
1029     * @param form the form.
1030     * @param allErrors the errors.
1031     * @param metadataSetElement the metadata set element for this metadata.
1032     * @param rawValues the raw values of the form
1033     * @param rawComments the raw comments of the form
1034     * @param repeaterDefinition the repeater definition.
1035     * @param metadataPath the metadata path.
1036     * @param externalAndLocalMetadata The paths of externalizable metadata
1037     * @throws WorkflowException if an error occurs.
1038     */
1039    protected void _bindAndValidateRepeater(Content content, Form form, AllErrors allErrors, AbstractMetadataSetElement metadataSetElement, Map<String, Object> rawValues, Map<String, List<Map<String, String>>> rawComments, RepeaterDefinition repeaterDefinition, String metadataPath, Set<String> externalAndLocalMetadata) throws WorkflowException
1040    {
1041        String metadataName = repeaterDefinition.getName();
1042        int repeaterSizeValue;
1043        String repeaterParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
1044        
1045        String repeaterSize = (String) rawValues.get(repeaterParamsPrefix + ".size");
1046        if (repeaterSize == null)
1047        {
1048            throw new WorkflowException("Missing request parameter size for metadata: " + metadataPath);
1049        }
1050        try
1051        {
1052            repeaterSizeValue = Integer.valueOf(repeaterSize);
1053        }
1054        catch (NumberFormatException e)
1055        {
1056            throw new WorkflowException("Invalid size: " + repeaterSize, e);
1057        }
1058
1059        RepeaterField repeaterField = new RepeaterField();
1060
1061        String repeaterMode = (String) rawValues.get(repeaterParamsPrefix + ".mode");
1062        repeaterField.setMode(repeaterMode);
1063
1064        for (int i = 1; i <= repeaterSizeValue; i++)
1065        {
1066            RepeaterEntry repeaterEntry = new RepeaterEntry();
1067            
1068            
1069            int previousPositionValue = -1;
1070            String previousPosition = (String) rawValues.get(repeaterParamsPrefix + "." + i + ".previous-position");
1071            if (previousPosition != null)
1072            {
1073                try
1074                {
1075                    previousPositionValue = Integer.valueOf(previousPosition);
1076                    repeaterEntry.setPreviousPosition(previousPositionValue);
1077                }
1078                catch (NumberFormatException e)
1079                {
1080                    throw new WorkflowException("Invalid position: " + previousPosition, e);
1081                }
1082            }
1083
1084            int positionValue = i;
1085            String position = (String) rawValues.get(repeaterParamsPrefix + "." + i + ".position");
1086            if (position != null)
1087            {
1088                try
1089                {
1090                    positionValue = Integer.valueOf(position);
1091                }
1092                catch (NumberFormatException e)
1093                {
1094                    throw new WorkflowException("Invalid position: " + position, e);
1095                }
1096            }
1097            repeaterEntry.setPosition(positionValue);
1098
1099            // Bind and validate each entry
1100            _bindAndValidateMetadataSetElement(content, repeaterEntry, allErrors, metadataSetElement, rawValues, rawComments, repeaterDefinition, metadataPath + "/" + i + "/", externalAndLocalMetadata);
1101            
1102            repeaterField.addEntry(repeaterEntry);
1103        }
1104
1105        // Size will be checked later
1106        
1107        form.setField(metadataName, repeaterField);
1108    }
1109
1110    /**
1111     * Bind and validate a string metadata.
1112     * @param allErrors for storing validation errors.
1113     * @param form the form. 
1114     * @param metadataDefinition the metadata definition.
1115     * @param content the content
1116     * @param metadataPath the metadata path from the content.
1117     * @param rawValue the submitted value.
1118     * @param rawValues the raw values of the form
1119     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
1120     * @throws AmetysRepositoryException if an error occurs.
1121     * @throws WorkflowException if an error occurs.
1122     */
1123    @SuppressWarnings("unchecked")
1124    protected void _bindAndValidateStringMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
1125    {
1126        String metadataName = metadataDefinition.getName();
1127
1128        AbstractField field = null;
1129        
1130        if (externalizable)
1131        {
1132            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
1133            SimpleField<String> localField = new SimpleField<>(localValues);
1134            
1135            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
1136            SimpleField<String> extField = new SimpleField<>(extValues);
1137            
1138            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
1139        }
1140        else
1141        {
1142            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
1143            field = new SimpleField<>(metadataValues);
1144        }
1145
1146        String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
1147        String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
1148        field.setMode(metadataMode);
1149
1150        String[] values = field instanceof ExternalizableField ? ((SimpleField<String>) ((ExternalizableField) field).getLocalField()).getValues() : ((SimpleField<String>) field).getValues();
1151        
1152        String[] valuesToValidate = values;
1153        if (field instanceof ExternalizableField && ((ExternalizableField) field).getStatus() == ExternalizableMetadataStatus.EXTERNAL)
1154        {
1155            // Validate external values
1156            valuesToValidate = ((SimpleField<String>) ((ExternalizableField) field).getExternalField()).getValues();
1157        }
1158        
1159        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, valuesToValidate))
1160        {
1161            if (metadataDefinition.isMultiple())
1162            {
1163                form.setField(metadataName, field);
1164            }
1165            else
1166            {
1167                if (externalizable || (values.length > 0 && !values[0].equals("")))
1168                {
1169                    form.setField(metadataName, field);
1170                }
1171            }
1172        }
1173    }
1174    
1175    /**
1176     * Bind and validate a multilingual string metadata.
1177     * @param allErrors for storing validation errors.
1178     * @param form the form. 
1179     * @param metadataDefinition the metadata definition.
1180     * @param content the content
1181     * @param metadataPath the metadata path from the content.
1182     * @param rawValue the submitted value.
1183     * @param rawValues the raw values of the form
1184     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
1185     * @throws AmetysRepositoryException if an error occurs.
1186     * @throws WorkflowException if an error occurs.
1187     */
1188    @SuppressWarnings("unchecked")
1189    protected void _bindAndValidateMultilingualStringMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
1190    {
1191        String metadataName = metadataDefinition.getName();
1192
1193        AbstractField field = null;
1194        
1195        if (externalizable)
1196        {
1197            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
1198            SimpleField<MultilingualString> localField = _bindMultilingualField(localValues);
1199            
1200            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
1201            SimpleField<MultilingualString> extField = _bindMultilingualField(extValues);
1202            
1203            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
1204        }
1205        else
1206        {
1207            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
1208            field = _bindMultilingualField(metadataValues);
1209        }
1210
1211        String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
1212        String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
1213        field.setMode(metadataMode);
1214
1215        MultilingualString[] values = field instanceof ExternalizableField ? ((SimpleField<MultilingualString>) ((ExternalizableField) field).getLocalField()).getValues() : ((SimpleField<MultilingualString>) field).getValues();
1216        
1217        MultilingualString[]  valuesToValidate = values;
1218        if (field instanceof ExternalizableField && ((ExternalizableField) field).getStatus() == ExternalizableMetadataStatus.EXTERNAL)
1219        {
1220            // Validate external values
1221            valuesToValidate = ((SimpleField<MultilingualString>) ((ExternalizableField) field).getExternalField()).getValues();
1222        }
1223        
1224        String[] strValuesToValidate = new String[0];
1225        if (valuesToValidate.length > 0)
1226        {
1227            List<String> strValues = valuesToValidate[0].getValues();
1228            strValuesToValidate = valuesToValidate[0].getValues().toArray(new String[strValues.size()]);
1229        }
1230        
1231        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, strValuesToValidate))
1232        {
1233            if (externalizable || (strValuesToValidate.length > 0 && !strValuesToValidate[0].equals("")))
1234            {
1235                form.setField(metadataName, field);
1236            }
1237        }
1238    }
1239    
1240    /**
1241     * Bind a multilingual field from form values
1242     * @param values the form values
1243     * @return The multilingual field
1244     */
1245    protected SimpleField<MultilingualString> _bindMultilingualField (String[] values)
1246    {
1247        if (values.length > 0 && !values[0].equals(""))
1248        {
1249            Map<String, Object> localesValues = _jsonUtils.convertJsonToMap(values[0]);
1250            
1251            MultilingualString multilingualString = new MultilingualString();
1252            for (Entry<String, Object> entry : localesValues.entrySet())
1253            {
1254                Object value = entry.getValue();
1255                if (value != null && StringUtils.isNotEmpty((String) value))
1256                {
1257                    multilingualString.add(LocaleUtils.toLocale(entry.getKey()), (String) value);
1258                }
1259            }
1260            
1261            return new SimpleField<>(new MultilingualString[] {multilingualString});
1262        }
1263        
1264        return new SimpleField<>(new MultilingualString[0]);
1265    }
1266    
1267    /**
1268     * Bind and validate a user metadata.
1269     * @param allErrors for storing validation errors.
1270     * @param form the form. 
1271     * @param content the content
1272     * @param metadataDefinition the metadata definition.
1273     * @param metadataPath the metadata path from the content.
1274     * @param rawValue the submitted value.
1275     * @param rawValues the raw values of the form
1276     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
1277     * @throws AmetysRepositoryException if an error occurs.
1278     * @throws WorkflowException if an error occurs.
1279     */
1280    protected void _bindAndValidateUserMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
1281    {
1282        String metadataName = metadataDefinition.getName();
1283        
1284        AbstractField field = null;
1285        if (externalizable)
1286        {
1287            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
1288            SimpleField<UserIdentity> localField = _bindUserField(localValues);
1289            
1290            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
1291            SimpleField<UserIdentity> extField = _bindUserField(extValues);
1292            
1293            if (localField != null)
1294            {
1295                field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
1296            }
1297        }
1298        else
1299        {
1300            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
1301            field = _bindUserField(metadataValues);
1302        }
1303        
1304        if (field != null)
1305        {
1306            String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replaceAll("/", ".");
1307            String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
1308            field.setMode(metadataMode);
1309            form.setField(metadataName, field);
1310        }
1311    }
1312    
1313    /**
1314     * Bind a user field from form values
1315     * @param values the form values
1316     * @return The user field
1317     */
1318    protected SimpleField<UserIdentity> _bindUserField (String[] values)
1319    {
1320        List<UserIdentity> users = new ArrayList<>();
1321        for (String value : values)
1322        {
1323            Map<String, Object> userValue = _jsonUtils.convertJsonToMap(value);
1324            
1325            UserIdentity userIdentity = _userHelper.json2userIdentity(userValue);
1326            if (userIdentity != null)
1327            {
1328                users.add(userIdentity);
1329            }
1330        }
1331        
1332        return new SimpleField<>(users.toArray(new UserIdentity[users.size()]));
1333    }
1334
1335    /**
1336     * Bind and validate a date metadata.
1337     * @param allErrors for storing validation errors.
1338     * @param form the form. 
1339     * @param content the content
1340     * @param metadataDefinition the metadata definition.
1341     * @param metadataPath the metadata path from the content.
1342     * @param rawValue the submitted value.
1343     * @param rawValues the raw values of the form
1344     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
1345     * @throws AmetysRepositoryException if an error occurs.
1346     * @throws WorkflowException if an error occurs.
1347     */
1348    @SuppressWarnings("unchecked")
1349    protected void _bindAndValidateDateMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
1350    {
1351        String metadataName = metadataDefinition.getName();
1352        
1353        AbstractField field = null;
1354        if (externalizable)
1355        {
1356            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
1357            SimpleField<Date> localField = _bindDateField(localValues, metadataPath, allErrors);
1358            
1359            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
1360            SimpleField<Date> extField = _bindDateField(extValues, metadataPath, allErrors);
1361            
1362            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
1363        }
1364        else
1365        {
1366            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
1367            field = _bindDateField(metadataValues, metadataPath, allErrors);
1368        }
1369        
1370        String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
1371        String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
1372        field.setMode(metadataMode);
1373        
1374        Date[] values = field instanceof ExternalizableField ? ((SimpleField<Date>) ((ExternalizableField) field).getLocalField()).getValues() : ((SimpleField<Date>) field).getValues();
1375        
1376        Date[] valuesToValidate = values;
1377        if (field instanceof ExternalizableField && ((ExternalizableField) field).getStatus() == ExternalizableMetadataStatus.EXTERNAL)
1378        {
1379            // Validate external values
1380            valuesToValidate = ((SimpleField<Date>) ((ExternalizableField) field).getExternalField()).getValues();
1381        }
1382        
1383        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, valuesToValidate))
1384        {
1385            if (metadataDefinition.isMultiple())
1386            {
1387                form.setField(metadataName, field);
1388            }
1389            else
1390            {
1391                if (externalizable || values.length > 0)
1392                {
1393                    form.setField(metadataName, field);
1394                }
1395            }
1396        }
1397    }
1398    
1399    /**
1400     * Bind a date field from form values
1401     * @param values the form values
1402     * @param metadataPath The path of metadata
1403     * @param allErrors for storing validation errors.
1404     * @return The date field
1405     */
1406    protected SimpleField<Date> _bindDateField (String[] values, String metadataPath, AllErrors allErrors)
1407    {
1408        List<Date> dateValues = new ArrayList<>();
1409        
1410        for (int i = 0; i < values.length; i++)
1411        {
1412            if (!"".equals(values[i]))
1413            {
1414                try
1415                {
1416                    LocalDate ld = LocalDate.parse(values[i], DateTimeFormatter.ISO_DATE_TIME);
1417                    dateValues.add(DateUtils.asDate(ld));
1418                }
1419                catch (DateTimeParseException e)
1420                {
1421                    Errors parseErrors = new Errors();
1422                    
1423                    List<String> parameters = new ArrayList<>();
1424                    parameters.add(values[i]);
1425                    parseErrors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_DATE_INVALID", parameters));
1426                    allErrors.addError(metadataPath, parseErrors);
1427                }
1428            }
1429        }
1430        
1431        Date[] dateArray = dateValues.toArray(new Date[dateValues.size()]);
1432        return new SimpleField<>(dateArray);
1433    }
1434    
1435    /**
1436     * Bind and validate a date time metadata.
1437     * @param allErrors for storing validation errors.
1438     * @param form the form. 
1439     * @param content the content
1440     * @param metadataDefinition the metadata definition.
1441     * @param metadataPath the metadata path from the content.
1442     * @param rawValue the submitted value.
1443     * @param rawValues the raw values of the form
1444     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
1445     * @throws AmetysRepositoryException if an error occurs.
1446     * @throws WorkflowException if an error occurs.
1447     */
1448    @SuppressWarnings("unchecked")
1449    protected void _bindAndValidateDateTimeMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
1450    {
1451        String metadataName = metadataDefinition.getName();
1452        
1453        AbstractField field = null;
1454        if (externalizable)
1455        {
1456            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
1457            SimpleField<Date> localField = _bindDateTimeField(localValues, metadataPath, allErrors);
1458            
1459            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
1460            SimpleField<Date> extField = _bindDateTimeField(extValues, metadataPath, allErrors);
1461            
1462            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
1463        }
1464        else
1465        {
1466            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
1467            field = _bindDateTimeField(metadataValues, metadataPath, allErrors);
1468        }
1469        
1470        String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
1471        String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
1472        field.setMode(metadataMode);
1473
1474        Date[] values = field instanceof ExternalizableField ? ((SimpleField<Date>) ((ExternalizableField) field).getLocalField()).getValues() : ((SimpleField<Date>) field).getValues();
1475        
1476        Date[] valuesToValidate = values;
1477        if (field instanceof ExternalizableField && ((ExternalizableField) field).getStatus() == ExternalizableMetadataStatus.EXTERNAL)
1478        {
1479            // Validate external values
1480            valuesToValidate = ((SimpleField<Date>) ((ExternalizableField) field).getExternalField()).getValues();
1481        }
1482        
1483        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, valuesToValidate))
1484        {
1485            if (metadataDefinition.isMultiple())
1486            {
1487                form.setField(metadataName, field);
1488            }
1489            else
1490            {
1491                if (externalizable || values.length > 0)
1492                {
1493                    form.setField(metadataName, field);
1494                }
1495            }
1496        }
1497    }
1498    
1499    /**
1500     * Bind a date time field from form values
1501     * @param values the form values
1502     * @param metadataPath The path of metadata
1503     * @param allErrors for storing validation errors.
1504     * @return The date field
1505     */
1506    protected SimpleField<Date> _bindDateTimeField (String[] values, String metadataPath, AllErrors allErrors)
1507    {
1508        List<Date> dateValues = new ArrayList<>();
1509        
1510        for (int i = 0; i < values.length; i++)
1511        {
1512            if (!"".equals(values[i]))
1513            {
1514                try
1515                {
1516                    LocalDateTime ld = LocalDateTime.parse(values[i], DateTimeFormatter.ISO_DATE_TIME);
1517                    dateValues.add(DateUtils.asDate(ld));
1518                }
1519                catch (DateTimeParseException e)
1520                {
1521                    Errors parseErrors = new Errors();
1522                    
1523                    List<String> parameters = new ArrayList<>();
1524                    parameters.add(values[i]);
1525                    parseErrors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_DATETIME_INVALID", parameters));
1526                    allErrors.addError(metadataPath, parseErrors);
1527                }
1528            }
1529        }
1530        
1531        Date[] dateArray = dateValues.toArray(new Date[dateValues.size()]);
1532        return new SimpleField<>(dateArray);
1533    }
1534
1535    /**
1536     * Bind and validate a long metadata.
1537     * @param allErrors for storing validation errors.
1538     * @param form the form. 
1539     * @param content the content
1540     * @param metadataDefinition the metadata definition.
1541     * @param metadataPath the metadata path from the content.
1542     * @param rawValue the submitted value.
1543     * @param rawValues the raw values of the form
1544     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
1545     * @throws AmetysRepositoryException if an error occurs.
1546     * @throws WorkflowException if an error occurs.
1547     */
1548    @SuppressWarnings("unchecked")
1549    protected void _bindAndValidateLongMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
1550    {
1551        String metadataName = metadataDefinition.getName();
1552        
1553        AbstractField field = null;
1554        if (externalizable)
1555        {
1556            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
1557            SimpleField<Long> localField = _bindLongField(localValues, metadataPath, allErrors);
1558            
1559            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
1560            SimpleField<Long> extField = _bindLongField(extValues, metadataPath, allErrors);
1561            
1562            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
1563        }
1564        else
1565        {
1566            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
1567            field = _bindLongField(metadataValues, metadataPath, allErrors);
1568        }
1569        
1570        String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
1571        String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
1572        field.setMode(metadataMode);
1573        
1574        Long[] values = field instanceof ExternalizableField ? ((SimpleField<Long>) ((ExternalizableField) field).getLocalField()).getValues() : ((SimpleField<Long>) field).getValues();
1575        
1576        Long[] valuesToValidate = values;
1577        if (field instanceof ExternalizableField && ((ExternalizableField) field).getStatus() == ExternalizableMetadataStatus.EXTERNAL)
1578        {
1579            // Validate external values
1580            valuesToValidate = ((SimpleField<Long>) ((ExternalizableField) field).getExternalField()).getValues();
1581        }
1582        
1583        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, valuesToValidate))
1584        {
1585            if (metadataDefinition.isMultiple())
1586            {
1587                form.setField(metadataName, field);
1588            }
1589            else
1590            {
1591                if (externalizable || values.length > 0)
1592                {
1593                    form.setField(metadataName, field);
1594                }
1595            }
1596        }
1597    }
1598    
1599    /**
1600     * Bind a long field from form values
1601     * @param values the form values
1602     * @param metadataPath The path of metadata
1603     * @param allErrors for storing validation errors.
1604     * @return The long field
1605     */
1606    protected SimpleField<Long> _bindLongField (String[] values, String metadataPath, AllErrors allErrors)
1607    {
1608        List<Long> longValues = new ArrayList<>();
1609        
1610        for (int i = 0; i < values.length; i++)
1611        {
1612            if (!"".equals(values[i]))
1613            {
1614                try
1615                {
1616                    longValues.add(Long.parseLong(values[i]));
1617                }
1618                catch (NumberFormatException e)
1619                {
1620                    Errors parseErrors = new Errors();
1621
1622                    List<String> parameters = new ArrayList<>();
1623                    parameters.add(values[i]);
1624                    parseErrors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_LONG_INVALID", parameters));
1625                    allErrors.addError(metadataPath, parseErrors);
1626                }
1627            }
1628        }
1629        
1630        Long[] longArray = longValues.toArray(new Long[longValues.size()]);
1631        return new SimpleField<>(longArray);
1632    }
1633    
1634    /**
1635     * Bind and validate a geocode metadata.
1636     * @param allErrors for storing validation errors.
1637     * @param form the form. 
1638     * @param content the content
1639     * @param metadataDefinition the metadata definition.
1640     * @param metadataPath the metadata path from the content.
1641     * @param rawValue the submitted value.
1642     * @param rawValues the raw values of the form
1643     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
1644     * @throws AmetysRepositoryException if an error occurs.
1645     * @throws WorkflowException if an error occurs.
1646     */
1647    protected void _bindAndValidateGeocodeMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
1648    {
1649        String metadataName = metadataDefinition.getName();
1650        
1651        AbstractField field = null;
1652        if (externalizable)
1653        {
1654            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
1655            SimpleField<Double> localField = _bindGeoCodeField(localValues);
1656            
1657            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
1658            SimpleField<Double> extField = _bindGeoCodeField(extValues);
1659            
1660            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
1661        }
1662        else
1663        {
1664            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
1665            field = _bindGeoCodeField(metadataValues);
1666        }
1667        
1668        String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
1669        String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
1670        
1671        // TODO Validate metadata. Need a DoubleValidator ??
1672        
1673        if (field != null)
1674        {
1675            field.setMode(metadataMode);
1676            form.setField(metadataName, field);
1677        }
1678    }
1679    
1680    /**
1681     * Bind a geocode field from form values
1682     * @param values the form values
1683     * @return The geocode field
1684     */
1685    protected SimpleField<Double> _bindGeoCodeField (String[] values)
1686    {
1687        if (values.length > 0)
1688        {
1689            Map<String, Object> geocodeValues = _jsonUtils.convertJsonToMap(values[0]);
1690            
1691            Number longitude = (Number) geocodeValues.get("longitude");
1692            Number latitude = (Number) geocodeValues.get("latitude");
1693            
1694            if (longitude != null && latitude != null)
1695            {
1696                return new SimpleField<>(new Double[]{longitude.doubleValue(), latitude.doubleValue()});
1697            }
1698        }
1699        return null;
1700    }
1701
1702    /**
1703     * Bind and validate a double metadata.
1704     * @param allErrors for storing validation errors.
1705     * @param form the form. 
1706     * @param content the content
1707     * @param metadataDefinition the metadata definition.
1708     * @param metadataPath the metadata path from the content.
1709     * @param rawValue the submitted value.
1710     * @param rawValues the raw values of the form
1711     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
1712     * @throws AmetysRepositoryException if an error occurs.
1713     * @throws WorkflowException if an error occurs.
1714     */
1715    @SuppressWarnings("unchecked")
1716    protected void _bindAndValidateDoubleMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
1717    {
1718        String metadataName = metadataDefinition.getName();
1719        
1720        AbstractField field = null;
1721        if (externalizable)
1722        {
1723            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
1724            SimpleField<Double> localField = _bindDoubleField(localValues, metadataPath, allErrors);
1725            
1726            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
1727            SimpleField<Double> extField = _bindDoubleField(extValues, metadataPath, allErrors);
1728            
1729            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
1730        }
1731        else
1732        {
1733            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
1734            field = _bindDoubleField(metadataValues, metadataPath, allErrors);
1735        }
1736        
1737        String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
1738        String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
1739        field.setMode(metadataMode);
1740        
1741        Double[] values = field instanceof ExternalizableField ? ((SimpleField<Double>) ((ExternalizableField) field).getLocalField()).getValues() : ((SimpleField<Double>) field).getValues();
1742        
1743        Double[] valuesToValidate = values;
1744        if (field instanceof ExternalizableField && ((ExternalizableField) field).getStatus() == ExternalizableMetadataStatus.EXTERNAL)
1745        {
1746            // Validate external values
1747            valuesToValidate = ((SimpleField<Double>) ((ExternalizableField) field).getExternalField()).getValues();
1748        }
1749        
1750        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, valuesToValidate))
1751        {
1752            if (metadataDefinition.isMultiple())
1753            {
1754                form.setField(metadataName, field);
1755            }
1756            else
1757            {
1758                if (externalizable || values.length != 0)
1759                {
1760                    form.setField(metadataName, field);
1761                }
1762            }
1763        }
1764    }
1765    
1766    /**
1767     * Bind a double field from form values
1768     * @param values the form values
1769     * @param metadataPath The path of metadata
1770     * @param allErrors for storing validation errors.
1771     * @return The double field
1772     */
1773    protected SimpleField<Double> _bindDoubleField (String[] values, String metadataPath, AllErrors allErrors)
1774    {
1775        List<Double> doubleValues = new ArrayList<>();
1776        
1777        for (int i = 0; i < values.length; i++)
1778        {
1779            if (!"".equals(values[i]))
1780            {
1781                try
1782                {
1783                    doubleValues.add(Double.parseDouble(values[i]));
1784                }
1785                catch (NumberFormatException e)
1786                {
1787                    Errors parseErrors = new Errors();
1788
1789                    List<String> parameters = new ArrayList<>();
1790                    parameters.add(values[i]);
1791                    parseErrors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_DOUBLE_INVALID", parameters));
1792                    allErrors.addError(metadataPath, parseErrors);
1793                }
1794            }
1795        }
1796        
1797        Double[] doubleArray = doubleValues.toArray(new Double[doubleValues.size()]);
1798        return new SimpleField<>(doubleArray);
1799    }
1800
1801    /**
1802     * Bind and validate a boolean metadata.
1803     * @param allErrors for storing validation errors.
1804     * @param form the form. 
1805     * @param content the content
1806     * @param metadataDefinition the metadata definition.
1807     * @param metadataPath the metadata path from the content.
1808     * @param rawValue the submitted value.
1809     * @param rawValues the raw values of the form
1810     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
1811     * @throws AmetysRepositoryException if an error occurs.
1812     * @throws WorkflowException if an error occurs.
1813     */
1814    @SuppressWarnings("unchecked")
1815    protected void _bindAndValidateBooleanMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
1816    {
1817        String metadataName = metadataDefinition.getName();
1818        
1819        AbstractField field = null;
1820        if (externalizable)
1821        {
1822            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
1823            SimpleField<Boolean> localField = _bindBooleanField(localValues, metadataPath, allErrors);
1824            
1825            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
1826            SimpleField<Boolean> extField = _bindBooleanField(extValues, metadataPath, allErrors);
1827            
1828            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
1829        }
1830        else
1831        {
1832            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
1833            field = _bindBooleanField(metadataValues, metadataPath, allErrors);
1834        }
1835        
1836        String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
1837        String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
1838        field.setMode(metadataMode);
1839
1840        Boolean[] values = field instanceof ExternalizableField ? ((SimpleField<Boolean>) ((ExternalizableField) field).getLocalField()).getValues() : ((SimpleField<Boolean>) field).getValues();
1841        
1842        Boolean[] valuesToValidate = values;
1843        if (field instanceof ExternalizableField && ((ExternalizableField) field).getStatus() == ExternalizableMetadataStatus.EXTERNAL)
1844        {
1845            // Validate external values
1846            valuesToValidate = ((SimpleField<Boolean>) ((ExternalizableField) field).getExternalField()).getValues();
1847        }
1848        
1849        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, valuesToValidate))
1850        {
1851            if (metadataDefinition.isMultiple())
1852            {
1853                form.setField(metadataName, field);
1854            }
1855            else
1856            {
1857                if (values.length != 0)
1858                {
1859                    form.setField(metadataName, field);
1860                }
1861            }
1862        }
1863    }
1864    
1865    /**
1866     * Bind a boolean field from form values
1867     * @param values the form values
1868     * @param metadataPath The path of metadata
1869     * @param allErrors for storing validation errors.
1870     * @return The boolean field
1871     */
1872    protected SimpleField<Boolean> _bindBooleanField (String[] values, String metadataPath, AllErrors allErrors)
1873    {
1874        List<Boolean> booleanValues = new ArrayList<>();
1875        
1876        for (int i = 0; i < values.length; i++)
1877        {
1878            try
1879            {
1880                booleanValues.add(Boolean.parseBoolean(values[i]));
1881            }
1882            catch (NumberFormatException e)
1883            {
1884                Errors parseErrors = new Errors();
1885                
1886                List<String> parameters = new ArrayList<>();
1887                parameters.add(values[i]);
1888                parseErrors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_BOOLEAN_INVALID", parameters));
1889                allErrors.addError(metadataPath, parseErrors);
1890            }
1891        }
1892        
1893        Boolean[] boolArray = booleanValues.toArray(new Boolean[booleanValues.size()]);
1894        return new SimpleField<>(boolArray);
1895    }
1896
1897    /**
1898     * Bind and validate a binary metadata.
1899     * @param allErrors for storing validation errors.
1900     * @param form the form. 
1901     * @param content the content
1902     * @param metadataDefinition the metadata definition.
1903     * @param metadataPath the metadata path from the content.
1904     * @param rawValue the submitted value.
1905     * @param rawValues the raw values of the form
1906     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
1907     * @throws AmetysRepositoryException if an error occurs.
1908     * @throws WorkflowException if an error occurs.
1909     */
1910    protected void _bindAndValidateBinaryMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
1911    {
1912        String metadataName = metadataDefinition.getName();
1913        // FIXME metadataPath is the right metadata path ?
1914        String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
1915        
1916        String[] realValues = new String[0];
1917        String[] valuesToValidate = new String[0];
1918        
1919        AbstractField field = null;
1920        if (externalizable)
1921        {
1922            realValues = _getLocalValues((String) rawValue, metadataDefinition);
1923            BinaryField localField = _bindBinaryField(realValues, metadataParamsPrefix, rawValues);
1924            
1925            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
1926            BinaryField extField = _bindBinaryField(extValues, metadataParamsPrefix, rawValues);
1927            
1928            ExternalizableMetadataStatus status = _getExternalizableStatus((String) rawValue);
1929            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
1930            
1931            valuesToValidate = status == ExternalizableMetadataStatus.EXTERNAL ? extValues : realValues;
1932        }
1933        else
1934        {
1935            realValues = _getMetadataValues(rawValue, metadataDefinition);
1936            valuesToValidate = realValues;
1937            field = _bindBinaryField(realValues, metadataParamsPrefix, rawValues);
1938        }
1939        
1940        String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
1941        field.setMode(metadataMode);
1942
1943        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, valuesToValidate))
1944        {
1945            if (externalizable || (realValues.length > 0 && !realValues[0].equals("")))
1946            {
1947                form.setField(metadataName, field);
1948            }
1949        }
1950    }
1951    
1952    /**
1953     * Bind a binary field from form values
1954     * @param values the form values
1955     * @param metadataParamsPrefix the prefix for metadata
1956     * @param rawValues The raw values
1957     * @return The binary field
1958     */
1959    protected BinaryField _bindBinaryField (String[] values, String metadataParamsPrefix, Map<String, Object> rawValues)
1960    {
1961        BinaryField field = new BinaryField(values);
1962        
1963        BinaryMetadata binaryValue = (BinaryMetadata) rawValues.get(metadataParamsPrefix + ".rawValue");
1964        if (binaryValue != null)
1965        {
1966            field.setBinaryValue(binaryValue);
1967        }
1968        
1969        return field;
1970    }
1971    
1972    /**
1973     * Bind and validate a file metadata.
1974     * @param allErrors for storing validation errors.
1975     * @param form the form. 
1976     * @param content the content
1977     * @param metadataDefinition the metadata definition.
1978     * @param metadataPath the metadata path from the content.
1979     * @param rawValue the submitted value.
1980     * @param rawValues the raw values of the form
1981     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
1982     * @throws AmetysRepositoryException if an error occurs.
1983     * @throws WorkflowException if an error occurs.
1984     */
1985    protected void _bindAndValidateFileMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
1986    {
1987        String metadataName = metadataDefinition.getName();
1988        // FIXME metadataPath is the right metadata path ?
1989        String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
1990        String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
1991        
1992        String[] realValues = new String[0];
1993        String[] valuesToValidate = new String[0];
1994        
1995        SimpleField<String> typeField = null;
1996        
1997        AbstractField field = null;
1998        if (externalizable)
1999        {
2000            realValues = _getLocalValues((String) rawValue, metadataDefinition);
2001            
2002            Map<String, Object> fileValues = _jsonUtils.convertJsonToMap(realValues[0]);
2003            String fileType = (String) fileValues.get("type");
2004            
2005            AbstractField localField;
2006            if (EXPLORER_FILE.equals(fileType))
2007            {
2008                String fileId = (String) fileValues.get("id");
2009                localField = new SimpleField<>(new String[]{fileId});
2010                typeField = new SimpleField<>(new String[]{EXPLORER_FILE});
2011            }
2012            else if (METADATA_FILE.equals(fileType))
2013            {
2014                String uploadId = (String) fileValues.get("id");
2015                localField = _bindBinaryField(new String[]{uploadId}, metadataParamsPrefix, rawValues);
2016                typeField = new SimpleField<>(new String[]{METADATA_FILE});
2017            }
2018            else
2019            {
2020                localField = new SimpleField<>(null);
2021            }
2022            
2023            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
2024            Map<String, Object> extFileValues = _jsonUtils.convertJsonToMap(realValues[0]);
2025            String extFileType = (String) extFileValues.get("type");
2026            
2027            AbstractField extField;
2028            if (EXPLORER_FILE.equals(extFileType))
2029            {
2030                String fileId = (String) extFileValues.get("id");
2031                extField = new SimpleField<>(new String[]{fileId});
2032            }
2033            else if (METADATA_FILE.equals(extFileType))
2034            {
2035                String uploadId = (String) extFileValues.get("id");
2036                extField = _bindBinaryField(new String[]{uploadId}, metadataParamsPrefix, rawValues);
2037            }
2038            else
2039            {
2040                extField = new SimpleField<>(null);
2041            }
2042            
2043            ExternalizableMetadataStatus status = _getExternalizableStatus((String) rawValue);
2044            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
2045            
2046            valuesToValidate = status == ExternalizableMetadataStatus.EXTERNAL ? extValues : realValues;
2047        }
2048        else
2049        {
2050            realValues = _getMetadataValues(rawValue, metadataDefinition);
2051            valuesToValidate = realValues;
2052            
2053            if (realValues.length > 0)
2054            {
2055                Map<String, Object> fileValues = _jsonUtils.convertJsonToMap(realValues[0]);
2056                String fileType = (String) fileValues.get("type");
2057                if (EXPLORER_FILE.equals(fileType))
2058                {
2059                    String fileId = (String) fileValues.get("id");
2060                    field = new SimpleField<>(new String[]{fileId});
2061                    typeField = new SimpleField<>(new String[]{EXPLORER_FILE});
2062                }
2063                else if (METADATA_FILE.equals(fileType))
2064                {
2065                    String uploadId = (String) fileValues.get("id");
2066                    field = _bindBinaryField(new String[]{uploadId}, metadataParamsPrefix, rawValues);
2067                    typeField = new SimpleField<>(new String[]{METADATA_FILE});
2068                }
2069                else
2070                {
2071                    field = new SimpleField<>(null);
2072                }
2073            }
2074            else
2075            {
2076                field = new SimpleField<>(null);
2077            }
2078        }
2079        
2080        field.setMode(metadataMode);
2081
2082        if (typeField != null)
2083        {
2084            typeField.setMode(metadataMode);
2085            form.setField(metadataDefinition.getName() + "#type", typeField);
2086        }
2087        
2088        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, valuesToValidate))
2089        {
2090            if (externalizable || (realValues.length > 0 && !realValues[0].equals("")))
2091            {
2092                form.setField(metadataName, field);
2093            }
2094        }
2095    }
2096    
2097    /**
2098     * Bind and validate a rich text metadata.
2099     * @param allErrors for storing validation errors.
2100     * @param form the form. 
2101     * @param content the content
2102     * @param metadataDefinition the metadata definition.
2103     * @param metadataPath the metadata path from the content.
2104     * @param rawValue the submitted value.
2105     * @param rawValues the raw values of the form
2106     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
2107     * @throws AmetysRepositoryException if an error occurs.
2108     * @throws WorkflowException if an error occurs.
2109     */
2110    protected void _bindAndValidateRichText(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
2111    {
2112        String metadataName = metadataDefinition.getName();
2113        String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
2114        
2115        AbstractField field = null;
2116        if (externalizable)
2117        {
2118            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
2119            RichTextField localField = _bindRichTextField(localValues, metadataParamsPrefix, rawValues);
2120            
2121            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
2122            RichTextField extField = _bindRichTextField(extValues, metadataParamsPrefix, rawValues);
2123            
2124            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
2125        }
2126        else
2127        {
2128            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
2129            field = _bindRichTextField(metadataValues, metadataParamsPrefix, rawValues);
2130        }
2131        
2132        String value = field instanceof ExternalizableField ? ((RichTextField) ((ExternalizableField) field).getLocalField()).getContent() : ((RichTextField) field).getContent();
2133        
2134        String valueToValidate = value;
2135        if (field instanceof ExternalizableField && ((ExternalizableField) field).getStatus() == ExternalizableMetadataStatus.EXTERNAL)
2136        {
2137            // Validate external values
2138            value = ((RichTextField) ((ExternalizableField) field).getExternalField()).getContent();
2139        }
2140        
2141        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, valueToValidate))
2142        {
2143            String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
2144            field.setMode(metadataMode);
2145            form.setField(metadataName, field);
2146        }
2147    }
2148    
2149    /**
2150     * Bind a richtext field from form values
2151     * @param values the form values
2152     * @param metadataParamsPrefix the prefix for metadata
2153     * @param rawValues The raw values
2154     * @return The richtext field
2155     */
2156    protected RichTextField _bindRichTextField (String[] values, String metadataParamsPrefix, Map<String, Object> rawValues)
2157    {
2158        if (values.length > 0 && !values[0].equals(""))
2159        {
2160            RichTextField field = new RichTextField(values[0]);
2161            
2162            String format = (String) rawValues.get(metadataParamsPrefix + ".format");
2163            if (StringUtils.isEmpty(format))
2164            {
2165                format = "html";
2166            }
2167            field.setFormat(format);
2168            
2169            @SuppressWarnings("unchecked")
2170            Map<String, Resource> addData = (Map<String, Resource>) rawValues.get(metadataParamsPrefix + ".additionalData");
2171            if (addData != null)
2172            {
2173                field.setAdditionalData(addData);
2174            }
2175            
2176            return field;
2177        }
2178        return new RichTextField(null);
2179    }
2180
2181    /**
2182     * Bind and validate a reference metadata.
2183     * @param allErrors for storing validation errors.
2184     * @param form the form. 
2185     * @param content the content
2186     * @param metadataDefinition the metadata definition.
2187     * @param metadataPath the metadata path from the content.
2188     * @param rawValue the submitted value.
2189     * @param rawValues the raw values of the form
2190     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
2191     * @throws AmetysRepositoryException if an error occurs.
2192     * @throws WorkflowException if an error occurs.
2193     */
2194    protected void _bindAndValidateReferenceMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
2195    {
2196        String metadataName = metadataDefinition.getName();
2197        
2198        AbstractField field = null;
2199        if (externalizable)
2200        {
2201            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
2202            ReferenceField localField = _bindReferenceField(localValues);
2203            
2204            String[] extValues = _getLocalValues((String) rawValue, metadataDefinition);
2205            ReferenceField extField = _bindReferenceField(extValues);
2206            
2207            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
2208        }
2209        else
2210        {
2211            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
2212            field = _bindReferenceField(metadataValues);
2213        }
2214        
2215        String value = field instanceof ExternalizableField ? ((ReferenceField) ((ExternalizableField) field).getLocalField()).getValue() : ((ReferenceField) field).getValue();
2216        
2217        String valueToValidate = value;
2218        if (field instanceof ExternalizableField && ((ExternalizableField) field).getStatus() == ExternalizableMetadataStatus.EXTERNAL)
2219        {
2220            // Validate external values
2221            value = ((ReferenceField) ((ExternalizableField) field).getExternalField()).getValue();
2222        }
2223        
2224        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, valueToValidate))
2225        {
2226            if (StringUtils.isNotEmpty(value))
2227            {
2228                form.setField(metadataName, field);
2229            }
2230        }
2231    }
2232    
2233    /**
2234     * Bind a reference field from form values
2235     * @param values the form values
2236     * @return The reference field
2237     */
2238    protected ReferenceField _bindReferenceField (String[] values)
2239    {
2240        Map<String, Object> refValues = Collections.emptyMap();
2241        if (values.length > 0)
2242        {
2243            refValues = _jsonUtils.convertJsonToMap(values[0]);
2244        }
2245        
2246        return new ReferenceField(refValues);
2247    }
2248    
2249    /**
2250     * Bind and validate a content reference metadata.
2251     * @param allErrors for storing validation errors.
2252     * @param form the form. 
2253     * @param content the content
2254     * @param metadataDefinition the metadata definition.
2255     * @param metadataPath the metadata path from the content.
2256     * @param rawValue the submitted value.
2257     * @param rawValues the raw values of the form
2258     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
2259     * @throws AmetysRepositoryException if an error occurs.
2260     * @throws WorkflowException if an error occurs.
2261     */
2262    @SuppressWarnings("unchecked")
2263    protected void _bindAndValidateContentReferenceMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
2264    {
2265        String metadataName = metadataDefinition.getName();
2266        
2267        String cTypeId = metadataDefinition.getContentType();
2268        Collection<String> validContentTypes = new HashSet<>();
2269        if (cTypeId != null)
2270        {
2271            validContentTypes.add(cTypeId);
2272            validContentTypes.addAll(_contentTypeExtensionPoint.getSubTypes(cTypeId));
2273        }
2274        
2275        AbstractField field = null;
2276        if (externalizable)
2277        {
2278            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
2279            SimpleField<Content> localField = _bindContentField(localValues, cTypeId, validContentTypes, metadataPath, allErrors);
2280            
2281            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
2282            SimpleField<Content> extField = _bindContentField(extValues, cTypeId, validContentTypes, metadataPath, allErrors);
2283            
2284            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
2285        }
2286        else
2287        {
2288            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
2289            field = _bindContentField(metadataValues, cTypeId, validContentTypes, metadataPath, allErrors);
2290        }
2291        
2292        Content[] contentValues = field instanceof ExternalizableField ? ((SimpleField<Content>) ((ExternalizableField) field).getLocalField()).getValues() : ((SimpleField<Content>) field).getValues();
2293        Content[] contentValuesToValidate = contentValues;
2294        if (field instanceof ExternalizableField && ((ExternalizableField) field).getStatus() == ExternalizableMetadataStatus.EXTERNAL)
2295        {
2296            // Validate external values
2297            contentValuesToValidate = ((SimpleField<Content>) ((ExternalizableField) field).getExternalField()).getValues();
2298        }
2299        
2300        String metadataParamsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
2301        String metadataMode = (String) rawValues.get(metadataParamsPrefix + ".mode");
2302        field.setMode(metadataMode);
2303        
2304        List<Content> oldValues = _getContentValues(content.getMetadataHolder(), metadataPath);
2305        List<Content> valuesToValidate = new ArrayList<>();
2306        
2307        if (field.getMode() == MODE.INSERT && metadataDefinition.isMultiple())
2308        {
2309            valuesToValidate.addAll(oldValues);
2310            valuesToValidate.addAll(Arrays.asList(contentValues));
2311        }
2312        else if (field.getMode() == MODE.REMOVE)
2313        {
2314            valuesToValidate.addAll(oldValues);
2315            valuesToValidate.removeAll(Arrays.asList(contentValues));
2316        }
2317        else
2318        {
2319            valuesToValidate.addAll(Arrays.asList(contentValuesToValidate));
2320        }
2321        
2322        if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, valuesToValidate.toArray(new Content[valuesToValidate.size()])))
2323        {
2324            if (metadataDefinition.isMultiple() || externalizable || contentValues.length > 0)
2325            {
2326                form.setField(metadataName, field);
2327            }
2328        }
2329    }
2330    
2331    /**
2332     * Bind a content field from form values
2333     * @param values the form values
2334     * @param cTypeId The id of content type
2335     * @param validContentTypes The valid content types
2336     * @param metadataPath The path of metadata
2337     * @param allErrors for storing validation errors.
2338     * @return The content field
2339     */
2340    protected SimpleField<Content> _bindContentField (String[] values, String cTypeId, Collection<String> validContentTypes, String metadataPath, AllErrors allErrors)
2341    {
2342        Set<Content> contentList = new LinkedHashSet<>();
2343        
2344        for (String value : values)
2345        {
2346            if (StringUtils.isNotEmpty(value))
2347            {
2348                try
2349                {
2350                    AmetysObject ao = _resolver.resolveById(value);
2351                    
2352                    if (ao instanceof Content)
2353                    {
2354                        Content contentMeta = (Content) ao;
2355                        
2356                        String[] contentCTypes = ArrayUtils.addAll(contentMeta.getTypes(), contentMeta.getMixinTypes());
2357                        if (cTypeId != null && CollectionUtils.intersection(validContentTypes, Arrays.asList(contentCTypes)).isEmpty())
2358                        {
2359                            Errors parseErrors = new Errors();
2360                            
2361                            List<String> parameters = new ArrayList<>();
2362                            parameters.add(_contentHelper.getTitle(contentMeta));
2363                            parameters.add(contentMeta.getId());
2364                            parameters.add(cTypeId);
2365                            parseErrors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_CONTENTREFERENCE_BADTYPED", parameters));
2366                            allErrors.addError(metadataPath, parseErrors);
2367                        }
2368                        else
2369                        {
2370                            contentList.add(contentMeta);
2371                        }
2372                    }
2373                    else
2374                    {
2375                        Errors parseErrors = new Errors();
2376                        
2377                        List<String> parameters = new ArrayList<>();
2378                        parameters.add(value);
2379                        parseErrors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_CONTENTREFERENCE_NOTCONTENT", parameters));
2380                        allErrors.addError(metadataPath, parseErrors);
2381                    }
2382                }
2383                catch (AmetysRepositoryException e)
2384                {
2385                    _logger.error(String.format("Content reference invalid at path '%s', value '%s'", metadataPath, value), e);
2386                    
2387                    Errors parseErrors = new Errors();
2388                    
2389                    List<String> parameters = new ArrayList<>();
2390                    parameters.add(value);
2391                    parseErrors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_CONTENTREFERENCE_INVALID", parameters));
2392                    allErrors.addError(metadataPath, parseErrors);
2393                }
2394            }
2395        }
2396        
2397        Content[] contentValues = contentList.toArray(new Content[contentList.size()]);
2398        
2399        return new SimpleField<>(contentValues);
2400    }
2401    
2402    /**
2403     * Get the content values
2404     * @param compositeMetadata The composite metadata
2405     * @param metadataPath The path of metadata
2406     * @return the list of content values
2407     */
2408    protected List<Content> _getContentValues (CompositeMetadata compositeMetadata, String metadataPath)
2409    {
2410        List<Content> values = new ArrayList<>();
2411        
2412        String[] pathSegments = StringUtils.split(metadataPath, ContentConstants.METADATA_PATH_SEPARATOR);
2413        
2414        if (pathSegments.length == 0)
2415        {
2416            return values;
2417        }
2418        
2419        CompositeMetadata parentCompositeMetadata = compositeMetadata;
2420        for (int i = 1;  i < pathSegments.length - 1; i++)
2421        {
2422            if (parentCompositeMetadata.hasMetadata(pathSegments[i]))
2423            {
2424                parentCompositeMetadata = parentCompositeMetadata.getCompositeMetadata(pathSegments[i]);
2425            }
2426            else
2427            {
2428                // Metadata does not exist, no content values
2429                return values;
2430            }
2431        }
2432        
2433        if (parentCompositeMetadata.hasMetadata(pathSegments[pathSegments.length - 1]))
2434        { 
2435            String[] contentIds = parentCompositeMetadata.getStringArray(pathSegments[pathSegments.length - 1], new String[0]);
2436            for (String contentId : contentIds)
2437            {
2438                if (StringUtils.isNotEmpty(contentId))
2439                {
2440                    try
2441                    {
2442                        Content content = _resolver.resolveById(contentId);
2443                        values.add(content);
2444                    }
2445                    catch (UnknownAmetysObjectException e)
2446                    {
2447                        _logger.warn("The content with id " + contentId + " does not exist anymore. It will be removed from metadata " + metadataPath);
2448                    }
2449                    catch (AmetysRepositoryException e)
2450                    {
2451                        throw new AmetysRepositoryException("Cannot edit metadata '" + metadataPath + "'", e);
2452                    }
2453                }
2454            }
2455        }
2456        
2457        return values;
2458    }
2459    
2460    /**
2461     * Bind and validate a content metadata.
2462     * @param allErrors for storing validation errors.
2463     * @param form the form. 
2464     * @param content the content
2465     * @param metadataDefinition the metadata definition.
2466     * @param metadataPath the metadata path from the content.
2467     * @param rawValue the submitted value.
2468     * @param rawValues the raw values.
2469     * @param externalizable <code>true</code> true if the metadata is an externalizable metadata (local and external value)
2470     * @throws AmetysRepositoryException if an error occurs.
2471     * @throws WorkflowException if an error occurs.
2472     */
2473    protected void _bindAndValidateSubContentMetadata(AllErrors allErrors, Form form, Content content, MetadataDefinition metadataDefinition, String metadataPath, Object rawValue, Map<String, Object> rawValues, boolean externalizable) throws AmetysRepositoryException, WorkflowException
2474    {
2475        String metadataName = metadataDefinition.getName();
2476        String cTypeId = metadataDefinition.getContentType();
2477        Collection<String> validContentTypes = new HashSet<>();
2478        if (cTypeId != null)
2479        {
2480            validContentTypes.add(cTypeId);
2481            validContentTypes.addAll(_contentTypeExtensionPoint.getSubTypes(cTypeId));
2482        }
2483        
2484        AbstractField field = null;
2485        if (externalizable)
2486        {
2487            String[] localValues = _getLocalValues((String) rawValue, metadataDefinition);
2488            SubContentField localField = _bindSubContentField(localValues, metadataDefinition, metadataPath, rawValues);
2489            
2490            String[] extValues = _getExternalValues((String) rawValue, metadataDefinition);
2491            SubContentField extField = _bindSubContentField(extValues, metadataDefinition, metadataPath, rawValues);
2492            
2493            field = new ExternalizableField(localField, extField, _getExternalizableStatus((String) rawValue));
2494        }
2495        else
2496        {
2497            String[] metadataValues = _getMetadataValues(rawValue, metadataDefinition);
2498            field = _bindSubContentField(metadataValues, metadataDefinition, metadataPath, rawValues);
2499        }
2500        
2501        if (field != null)
2502        {
2503            List<Map<String, Object>> contentValues = field instanceof ExternalizableField ? ((SubContentField) ((ExternalizableField) field).getLocalField()).getContentValues() : ((SubContentField) field).getContentValues();
2504            
2505            List<Map<String, Object>> contentValuesToValidate = contentValues;
2506            if (field instanceof ExternalizableField && ((ExternalizableField) field).getStatus() == ExternalizableMetadataStatus.EXTERNAL)
2507            {
2508                // Validate external values
2509                contentValuesToValidate = ((SubContentField) ((ExternalizableField) field).getExternalField()).getContentValues();
2510            }
2511            
2512            String paramsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
2513            String metadataMode = (String) rawValues.get(paramsPrefix + ".mode");
2514            field.setMode(metadataMode);
2515            
2516            if (_validateMetadata(content, metadataDefinition, metadataPath, allErrors, contentValuesToValidate))
2517            {
2518                form.setField(metadataName, field);
2519            }
2520            
2521        }
2522    }
2523    
2524    /**
2525     * Bind a sub-content field from form values
2526     * @param values the form values
2527     * @param metadataDef The metadata definition
2528     * @param metadataPath The path of metadata
2529     * @param rawValues The raw values
2530     * @return The sub-content field
2531     */
2532    protected SubContentField _bindSubContentField (String[] values, MetadataDefinition metadataDef, String metadataPath, Map<String, Object> rawValues)
2533    {
2534        List<Map<String, Object>> contentValues = new ArrayList<>();
2535        Set<String> contentIds = new HashSet<>();
2536        
2537        for (String metadataValue : values)
2538        {
2539            Map<String, Object> contentValue = _jsonUtils.convertJsonToMap(metadataValue);
2540            
2541            String contentId = null;
2542            if (contentValue.containsKey("id"))
2543            {
2544                // Avoid duplicates id
2545                contentId = (String) contentValue.get("id");
2546                if (!contentIds.contains(contentId))
2547                {
2548                    contentIds.add(contentId);
2549                    contentValues.add(contentValue);
2550                }
2551            }
2552            else
2553            {
2554                contentValues.add(contentValue);
2555            }
2556        }
2557        
2558        if (metadataDef.isMultiple() || !contentValues.isEmpty())
2559        {
2560            String paramsPrefix = INTERNAL_FORM_ELEMENTS_PREFIX + metadataPath.replace('/', '.');
2561            String contentLanguage = (String) rawValues.get(paramsPrefix + ".contentLanguage");
2562            Integer initWorkflowActionId = (Integer) rawValues.get(paramsPrefix + ".initWorkflowActionId");
2563            String workflowName = (String) rawValues.get(paramsPrefix + ".workflowName");
2564            
2565            SubContentField subContentField = new SubContentField(contentValues, contentLanguage);
2566            subContentField.setWorkflow(workflowName, initWorkflowActionId);
2567            
2568            return subContentField;
2569        }
2570        
2571        return null;
2572    }
2573    
2574    /**
2575     * Validate a metadata value.
2576     * @param content the content
2577     * @param metadataDefinition the metadata definition.
2578     * @param metadataPath the metadata path.
2579     * @param allErrors the errors.
2580     * @param value the value.
2581     * @return <code>true</code> if the validation is successful,
2582     *         <code>false</code> otherwise.
2583     * @throws WorkflowException if an error occurs.
2584     */
2585    protected boolean _validateMetadata(Content content, MetadataDefinition metadataDefinition, String metadataPath, AllErrors allErrors, Object value) throws WorkflowException
2586    {
2587        Validator validator = metadataDefinition.getValidator();
2588        
2589        if (validator != null)
2590        {
2591            Errors errors = new Errors();
2592            
2593            if (value != null && value.getClass().isArray() && !metadataDefinition.isMultiple())
2594            {
2595                Object singleValue = null;
2596                if (Array.getLength(value) != 0)
2597                {
2598                    singleValue = Array.get(value, 0);
2599                }
2600                
2601                validator.validate(singleValue, errors);
2602            }
2603            else
2604            {
2605                validator.validate(value, errors);
2606            }
2607            
2608            if (errors.hasErrors())
2609            {
2610                allErrors.addError(metadataPath, errors);
2611                
2612                return false;
2613            }
2614        }
2615        
2616        return true;
2617    }
2618
2619    /**
2620     * Prepare to synchronize a metadata with a composite metadata.
2621     * @param content the content.
2622     * @param metadata the composite metadata to synchronize.
2623     * @param form the form.
2624     * @param allErrors the errors.
2625     * @param user the user.
2626     * @param metadataSetElement the metadata set element for this metadata.
2627     * @param metadataDefinition the metadata definition.
2628     * @param metadataPath the metadata path.
2629     * @param invertEditActionId The action id for editing invert relation
2630     * @throws WorkflowException if an error occurs.
2631     * @throws AmetysRepositoryException If an error occurred 
2632     */
2633    protected void _prepareSynchronizeMetadata(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metadataDefinition, String metadataPath, int invertEditActionId) throws WorkflowException, AmetysRepositoryException
2634    {
2635        MetadataType type = metadataDefinition.getType();
2636        switch (type)
2637        {
2638            case CONTENT:
2639                _prepareSynchronizeContentReferenceMetadata(content, metadata, form, allErrors, user, metadataDefinition, metadataPath, invertEditActionId);
2640                break;
2641
2642            case COMPOSITE:
2643                _prepareSynchronizeCompositeMetadata(content, metadata, form, allErrors, user, metadataSetElement, metadataDefinition, metadataPath, invertEditActionId);
2644                break;
2645                
2646            default:
2647                break;
2648        }
2649    }
2650    
2651    /**
2652     * Synchronize a metadata with a composite metadata.
2653     * @param content the content.
2654     * @param metadata the composite metadata to synchronize.
2655     * @param form the form.
2656     * @param allErrors the errors.
2657     * @param user the user.
2658     * @param metadataSetElement the metadata set element for this metadata.
2659     * @param metadataDefinition the metadata definition.
2660     * @param metadataPath the metadata path.
2661     * @param invertEditActionId The action id for editing invert relation
2662     * @param externalAndLocalMetadata The paths of local and externam metadata
2663     * @throws WorkflowException if an error occurs.
2664     */
2665    protected void _synchronizeMetadata(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metadataDefinition, String metadataPath, int invertEditActionId, Set<String> externalAndLocalMetadata) throws WorkflowException
2666    {
2667        MetadataType type = metadataDefinition.getType();
2668        
2669        boolean externalizable = externalAndLocalMetadata.contains(metadataPath);
2670        
2671        switch (type)
2672        {
2673            case STRING:
2674                _synchronizeStringMetadata(metadata, form, metadataDefinition, externalizable);
2675                break;
2676            case MULTILINGUAL_STRING:
2677                _synchronizeMultilingualStringMetadata(metadata, form, metadataDefinition, externalizable);
2678                break;
2679            case USER:
2680                _synchronizeUserMetadata(metadata, form, metadataDefinition, externalizable);
2681                break;
2682            case DATE:
2683                _synchronizeDateMetadata(metadata, form, metadataDefinition, externalizable);
2684                break;
2685            case DATETIME:
2686                _synchronizeDateMetadata(metadata, form, metadataDefinition, externalizable);
2687                break;
2688            case LONG:
2689                _synchronizeLongMetadata(metadata, form, metadataDefinition, externalizable);
2690                break;
2691            case DOUBLE:
2692                _synchronizeDoubleMetadata(metadata, form, metadataDefinition, externalizable);
2693                break;
2694            case BOOLEAN:
2695                _synchronizeBooleanMetadata(metadata, form, metadataDefinition, externalizable);
2696                break;
2697            case BINARY:
2698                _synchronizeBinaryMetadata(metadata, form, allErrors, user, metadataDefinition, metadataPath, externalizable);
2699                break;
2700            case FILE:
2701                _synchronizeFileMetadata(metadata, form, allErrors, user, metadataDefinition, metadataPath, externalizable);
2702                break;
2703            case RICH_TEXT:
2704                _synchronizeRichTextMetadata(metadata, form, allErrors, user, metadataDefinition, metadataPath, externalizable);
2705                break;
2706            case CONTENT:
2707                _synchronizeContentReferenceMetadata(content, metadata, form, allErrors, user, metadataDefinition, metadataPath, invertEditActionId, externalizable);
2708                break;
2709            case SUB_CONTENT:
2710                _synchronizeSubContentMetadata(content, metadata, form, allErrors, user, metadataDefinition, metadataPath, externalizable);
2711                break;
2712            case GEOCODE:
2713                _synchronizeGeocodeMetadata(metadata, form, metadataDefinition, externalizable);
2714                break;
2715            case REFERENCE:
2716                _synchronizeReferenceMetadata(metadata, form, metadataDefinition, externalizable);
2717                break;
2718            case COMPOSITE:
2719                _synchronizeCompositeMetadata(content, metadata, form, allErrors, user, metadataSetElement, metadataDefinition, metadataPath, invertEditActionId, externalAndLocalMetadata);
2720                break;
2721            default:
2722                throw new WorkflowException("Unsupported type: " + type);
2723        }
2724        
2725        // Synchronize metadata comments
2726        _synchronizeMetadataComments(metadata, form, metadataDefinition);
2727    }
2728    
2729    /**
2730     * Synchronize the comments of a field.
2731     * @param metadata the metadata.
2732     * @param form the form containing the field.
2733     * @param metadataDefinition the metadata definition.
2734     */
2735    protected void _synchronizeMetadataComments(ModifiableCompositeMetadata metadata, Form form, MetadataDefinition metadataDefinition)
2736    {
2737        String metadataName = metadataDefinition.getName();
2738        MetadataComment[] comments = form.getCommentArray(metadataName);
2739        
2740        if (metadata instanceof CommentableCompositeMetadata)
2741        {
2742            CommentableCompositeMetadata commentableMetadata = (CommentableCompositeMetadata) metadata;
2743            int index = 1;
2744            
2745            // Do not modify comments if no info. However, with an empty array, all existing comments will be removed.
2746            if (comments != null)
2747            {
2748                // Add / edit comments
2749                for (MetadataComment comment : comments)
2750                {
2751                    if (commentableMetadata.hasComment(metadataName, index))
2752                    {
2753                        commentableMetadata.editComment(metadataName, index, comment.getComment(), comment.getAuthor(), comment.getDate());
2754                    }
2755                    else
2756                    {
2757                        commentableMetadata.addComment(metadataName, comment.getComment(), comment.getAuthor(), comment.getDate());
2758                    }
2759                    
2760                    index++;
2761                }
2762                
2763                // Delete remaining comments
2764                while (commentableMetadata.hasComment(metadataName, index))
2765                {
2766                    commentableMetadata.deleteComment(metadataName, index);
2767                    index++;
2768                }
2769            }
2770        }
2771
2772    }
2773
2774    /**
2775     * Synchronize a composite-typed metadata with a a composite metadata.
2776     * @param content the content.
2777     * @param metadata the composite metadata to synchronize.
2778     * @param form the form.
2779     * @param allErrors the errors.
2780     * @param user the user.
2781     * @param metadataSetElement the metadata set element for this metadata.
2782     * @param metadataDefinition the metadata definition.
2783     * @param metadataPath the metadata path.
2784     * @param editActionId The action id for editing invert relation
2785     * @throws WorkflowException if an error occurs.
2786     */
2787    protected void _prepareSynchronizeCompositeMetadata(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metadataDefinition, String metadataPath, int editActionId) throws WorkflowException
2788    {
2789        String metadataName = metadataDefinition.getName();
2790        
2791        if (metadataDefinition instanceof RepeaterDefinition)
2792        {
2793            _prepareSynchronizeRepeater(content, metadata, form, allErrors, user, metadataSetElement, (RepeaterDefinition) metadataDefinition, metadataPath, editActionId);
2794        }
2795        else 
2796        {
2797            Form compositeForm = form.getCompositeField(metadataName);
2798            
2799            if (compositeForm != null)
2800            {
2801                ModifiableCompositeMetadata subMetadata = metadata.getCompositeMetadata(metadataName, true);
2802                _prepareSynchronizeMetadataSetElement(content, subMetadata, compositeForm, allErrors, user, metadataSetElement, metadataDefinition, metadataPath + ContentConstants.METADATA_PATH_SEPARATOR, editActionId);
2803            }
2804            else
2805            {
2806                if (metadata.hasMetadata(metadataName))
2807                {
2808                    metadata.removeMetadata(metadataName);
2809                }
2810            }
2811        }
2812    }
2813    
2814    /**
2815     * Synchronize a composite-typed metadata with a a composite metadata.
2816     * @param content the content.
2817     * @param metadata the composite metadata to synchronize.
2818     * @param form the form.
2819     * @param allErrors the errors.
2820     * @param user the user.
2821     * @param metadataSetElement the metadata set element for this metadata.
2822     * @param metadataDefinition the metadata definition.
2823     * @param metadataPath the metadata path.
2824     * @param editActionId The action id for editing invert relation
2825     * @param externalAndLocalMetadata The paths of local and externam metadata
2826     * @throws WorkflowException if an error occurs.
2827     */
2828    protected void _synchronizeCompositeMetadata(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, AbstractMetadataSetElement metadataSetElement, MetadataDefinition metadataDefinition, String metadataPath, int editActionId, Set<String> externalAndLocalMetadata) throws WorkflowException
2829    {
2830        String metadataName = metadataDefinition.getName();
2831        
2832        if (metadataDefinition instanceof RepeaterDefinition)
2833        {
2834            _synchronizeRepeater(content, metadata, form, allErrors, user, metadataSetElement, (RepeaterDefinition) metadataDefinition, metadataPath, editActionId, externalAndLocalMetadata);
2835        }
2836        else 
2837        {
2838            Form compositeForm = form.getCompositeField(metadataName);
2839            
2840            if (compositeForm != null)
2841            {
2842                ModifiableCompositeMetadata subMetadata = metadata.getCompositeMetadata(metadataName, true);
2843                _synchronizeMetadataSetElement(content, subMetadata, compositeForm, allErrors, user, metadataSetElement, metadataDefinition, metadataPath + ContentConstants.METADATA_PATH_SEPARATOR, editActionId, externalAndLocalMetadata);
2844            }
2845            else
2846            {
2847                if (metadata.hasMetadata(metadataName))
2848                {
2849                    metadata.removeMetadata(metadataName);
2850                }
2851            }
2852        }
2853    }
2854    
2855    /**
2856     * Synchronize a repeater with a composite metadata.
2857     * @param content the content.
2858     * @param metadata the composite metadata to synchronize.
2859     * @param form the form.
2860     * @param allErrors the errors.
2861     * @param user the user.
2862     * @param metadataSetElement the metadata set element for this metadata.
2863     * @param repeaterDefinition the repeater definition.
2864     * @param metadataPath the metadata path.
2865     * @param invertActionId The current 'edit content' action ID.
2866     * @throws WorkflowException if an error occurs.
2867     */
2868    protected void _prepareSynchronizeRepeater(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, AbstractMetadataSetElement metadataSetElement, RepeaterDefinition repeaterDefinition, String metadataPath, int invertActionId) throws WorkflowException
2869    {
2870        String metadataName = repeaterDefinition.getName();
2871        RepeaterField repeaterField = form.getRepeaterField(metadataName);
2872        
2873        List<RepeaterEntry> entries = repeaterField.getEntries();
2874        
2875        ModifiableCompositeMetadata repeaterMetadata = metadata.getCompositeMetadata(metadataName, true);
2876        
2877        // Add new entries
2878        for (RepeaterEntry repeaterEntry : entries)
2879        {
2880            int computedPosition = _computeRepeaterEntryPosition(repeaterMetadata, repeaterEntry);
2881
2882            try
2883            {
2884                ModifiableCompositeMetadata entryMetadata = repeaterMetadata.getCompositeMetadata(String.valueOf(computedPosition));
2885                _prepareSynchronizeMetadataSetElement(content, entryMetadata, repeaterEntry, allErrors, user, metadataSetElement, repeaterDefinition, metadataPath + "/" + computedPosition + "/", invertActionId);
2886            }
2887            catch (UnknownMetadataException e)
2888            {
2889                ModifiableCompositeMetadata entryMetadata = repeaterMetadata.getCompositeMetadata(String.valueOf(computedPosition), true);
2890                _prepareSynchronizeMetadataSetElement(content, entryMetadata, repeaterEntry, allErrors, user, metadataSetElement, repeaterDefinition, metadataPath + "/" + computedPosition + "/", invertActionId);
2891
2892                // Then remove created entry
2893                repeaterMetadata.removeMetadata(String.valueOf(computedPosition));
2894            }
2895        }
2896    }
2897    
2898    
2899    /**
2900     * Synchronize a repeater with a composite metadata.
2901     * @param content the content.
2902     * @param metadata the composite metadata to synchronize.
2903     * @param form the form.
2904     * @param allErrors the errors.
2905     * @param user the user.
2906     * @param metadataSetElement the metadata set element for this metadata.
2907     * @param repeaterDefinition the repeater definition.
2908     * @param metadataPath the metadata path.
2909     * @param editActionId The current 'edit content' action ID.
2910     * @param externalAndLocalMetadata The paths of local and externam metadata
2911     * @throws WorkflowException if an error occurs.
2912     */
2913    protected void _synchronizeRepeater(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, AbstractMetadataSetElement metadataSetElement, RepeaterDefinition repeaterDefinition, String metadataPath, int editActionId, Set<String> externalAndLocalMetadata) throws WorkflowException
2914    {
2915        String metadataName = repeaterDefinition.getName();
2916        RepeaterField repeaterField = form.getRepeaterField(metadataName);
2917        
2918        _checkRepeaterSize(metadata, form, allErrors, repeaterDefinition, metadataPath);
2919
2920        switch (repeaterField.getMode())
2921        {
2922            case REPLACE:
2923                _synchronizeRepeaterInReplaceMode(content, metadata, form, allErrors, user, metadataSetElement, repeaterDefinition, metadataPath, editActionId, externalAndLocalMetadata);
2924                break;
2925            case REMOVE:
2926                _synchronizeRepeaterInRemoveMode(content, metadata, form, allErrors, user, metadataSetElement, repeaterDefinition, metadataPath);
2927                break;
2928            case INSERT:
2929            default:
2930                _synchronizeRepeaterInInsertMode(content, metadata, form, allErrors, user, metadataSetElement, repeaterDefinition, metadataPath, editActionId, externalAndLocalMetadata);
2931                break;
2932        }
2933    }
2934    
2935    /**
2936     * Check the repeater size will be correct
2937     * @param metadata The metadata values
2938     * @param form the form
2939     * @param allErrors the list of errors
2940     * @param repeaterDefinition the definition of the repeater
2941     * @param metadataPath The path of the metadata
2942     */
2943    protected void _checkRepeaterSize(ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, RepeaterDefinition repeaterDefinition, String metadataPath)
2944    {
2945        String metadataName = repeaterDefinition.getName();
2946        ModifiableCompositeMetadata repeaterMetadata = metadata.getCompositeMetadata(metadataName, true);
2947        RepeaterField repeaterField = form.getRepeaterField(metadataName);
2948        
2949        // Check size
2950        int minSize = repeaterDefinition.getMinSize();
2951        
2952        int newSize = (repeaterField.getMode() == MODE.REPLACE ? repeaterField.getEntries().size() : repeaterMetadata.getMetadataNames().length)
2953                + (repeaterField.getMode() == MODE.INSERT ? repeaterField.getEntries().size() : 0)
2954                + (repeaterField.getMode() == MODE.REMOVE ? -repeaterField.getEntries().size() : 0);
2955
2956        // Min size validation
2957        if (newSize < minSize)
2958        {
2959            Errors errors = new Errors();
2960            
2961            List<String> parameters = new ArrayList<>();
2962            parameters.add(metadataName);
2963            parameters.add(Integer.toString(minSize));
2964            errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_REPEATER_MINSIZE", parameters));
2965            allErrors.addError(metadataPath, errors);
2966        }
2967        // Max size validation
2968        int maxSize = repeaterDefinition.getMaxSize();
2969        if (maxSize > 0 && newSize > maxSize)
2970        {
2971            Errors errors = new Errors();
2972
2973            List<String> parameters = new ArrayList<>();
2974            parameters.add(metadataName);
2975            parameters.add(Integer.toString(maxSize));
2976            errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_REPEATER_MAXSIZE", parameters));
2977            allErrors.addError(metadataPath, errors);
2978        }
2979    }
2980
2981    /**
2982     * Synchronize a repeater with a composite metadata, when the values has to be added to existing ones.
2983     * @param content the content.
2984     * @param metadata the composite metadata to synchronize.
2985     * @param form the form.
2986     * @param allErrors the errors.
2987     * @param user the user.
2988     * @param metadataSetElement the metadata set element for this metadata.
2989     * @param repeaterDefinition the repeater definition.
2990     * @param metadataPath the metadata path.
2991     * @param editActionId The current 'edit content' action ID.
2992     * @param externalAndLocalMetadata The paths of local and externam metadata
2993     * @throws WorkflowException if an error occurs.
2994     */
2995    protected void _synchronizeRepeaterInInsertMode(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, AbstractMetadataSetElement metadataSetElement, RepeaterDefinition repeaterDefinition, String metadataPath, int editActionId, Set<String> externalAndLocalMetadata) throws WorkflowException
2996    {
2997        String metadataName = repeaterDefinition.getName();
2998        RepeaterField repeaterField = form.getRepeaterField(metadataName);
2999        List<RepeaterEntry> entries = repeaterField.getEntries();
3000        
3001        ModifiableCompositeMetadata repeaterMetadata = metadata.getCompositeMetadata(metadataName, true);
3002        
3003        // Add new entries
3004        for (RepeaterEntry repeaterEntry : entries)
3005        {
3006            int computedPosition = _computeRepeaterEntryPosition(repeaterMetadata, repeaterEntry);
3007            
3008            // Is somebody in the way?
3009            for (int i = repeaterMetadata.getMetadataNames().length; i >= computedPosition; i--)
3010            {
3011                // move to the next position
3012                _moveRepeaterEntry(repeaterMetadata, String.valueOf(i), String.valueOf(i + 1));
3013            }
3014
3015            // New entry (computedPosition is free)
3016            ModifiableCompositeMetadata entryMetadata = repeaterMetadata.getCompositeMetadata(String.valueOf(computedPosition), true);
3017            _synchronizeMetadataSetElement(content, entryMetadata, repeaterEntry, allErrors, user, metadataSetElement, repeaterDefinition, metadataPath + "/" + computedPosition + "/", editActionId, externalAndLocalMetadata);
3018        }
3019    }
3020
3021    /**
3022     * Synchronize a repeater with a composite metadata, when the values has to removed from existing ones.
3023     * @param content the content.
3024     * @param metadata the composite metadata to synchronize.
3025     * @param form the form.
3026     * @param allErrors the errors.
3027     * @param user the user.
3028     * @param metadataSetElement the metadata set element for this metadata.
3029     * @param repeaterDefinition the repeater definition.
3030     * @param metadataPath the metadata path.
3031     * @throws WorkflowException if an error occurs.
3032     */
3033    protected void _synchronizeRepeaterInRemoveMode(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, AbstractMetadataSetElement metadataSetElement, RepeaterDefinition repeaterDefinition, String metadataPath) throws WorkflowException
3034    {
3035        String metadataName = repeaterDefinition.getName();
3036        RepeaterField repeaterField = form.getRepeaterField(metadataName);
3037        List<RepeaterEntry> entries = repeaterField.getEntries();
3038        
3039        ModifiableCompositeMetadata repeaterMetadata = metadata.getCompositeMetadata(metadataName, true);
3040        
3041        // Add new entries
3042        for (RepeaterEntry repeaterEntry : entries)
3043        {
3044            int computedPosition = _computeRepeaterEntryPosition(repeaterMetadata, repeaterEntry);
3045
3046            // remove
3047            repeaterMetadata.removeMetadata(String.valueOf(computedPosition));
3048
3049            // Is there a hole?
3050            for (int i = computedPosition + 1; i < repeaterMetadata.getMetadataNames().length; i++)
3051            {
3052                // move to the next position
3053                _moveRepeaterEntry(repeaterMetadata, String.valueOf(i), String.valueOf(i - 1));
3054            }
3055        }
3056
3057    }
3058
3059    private int _computeRepeaterEntryPosition(ModifiableCompositeMetadata repeaterMetadata, RepeaterEntry repeaterEntry)
3060    {
3061        int position = repeaterEntry.getPosition();
3062        // -1, -2, -3... means 1, 2, 3 before the end
3063        // 0 means at the end
3064        // 1, 2, 3... means at position 1, 2 or 3
3065        int computedPosition;
3066        if (position > 0)
3067        {
3068            computedPosition = position;
3069        }
3070        else
3071        {
3072            computedPosition = repeaterMetadata.getMetadataNames().length + 1 - position;
3073        }
3074        
3075        computedPosition = Math.max(Math.min(computedPosition, repeaterMetadata.getMetadataNames().length + 1), 1);
3076        return computedPosition;
3077    }
3078    
3079    /**
3080     * Synchronize a repeater with a composite metadata, when the values has to replace existing ones.
3081     * @param content the content.
3082     * @param metadata the composite metadata to synchronize.
3083     * @param form the form.
3084     * @param allErrors the errors.
3085     * @param user the user.
3086     * @param metadataSetElement the metadata set element for this metadata.
3087     * @param repeaterDefinition the repeater definition.
3088     * @param metadataPath the metadata path.
3089     * @param editActionId The current 'edit content' action ID.
3090     * @param externalAndLocalMetadata The paths of local and externam metadata
3091     * @throws WorkflowException if an error occurs.
3092     */
3093    protected void _synchronizeRepeaterInReplaceMode(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, AbstractMetadataSetElement metadataSetElement, RepeaterDefinition repeaterDefinition, String metadataPath, int editActionId, Set<String> externalAndLocalMetadata) throws WorkflowException
3094    {
3095        String metadataName = repeaterDefinition.getName();
3096        RepeaterField repeaterField = form.getRepeaterField(metadataName);
3097        List<RepeaterEntry> entries = repeaterField.getEntries();
3098        Map<String, String> laterMoves = new HashMap<>();
3099        
3100        ModifiableCompositeMetadata repeaterMetadata = metadata.getCompositeMetadata(metadataName, true);
3101
3102        for (String entryName : repeaterMetadata.getMetadataNames())
3103        {
3104            // Only process composite metadata
3105            if (repeaterMetadata.getType(entryName) == org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType.COMPOSITE)
3106            {
3107                // -1 is returns for custom composite metadata on a repeater which is not an entry
3108                int currentPosition = NumberUtils.toInt(entryName, -1);
3109
3110                if (currentPosition != -1)
3111                {
3112                    RepeaterEntry repeaterEntry = _getEntry(entries, entryName);
3113                    
3114                    if (repeaterEntry == null)
3115                    {
3116                        // Do additional processing to remove entry sub-metadatas.
3117                        ModifiableCompositeMetadata entryMetadata = repeaterMetadata.getCompositeMetadata(entryName);
3118                        _synchronizeMetadataRemoval(content, entryMetadata, repeaterEntry, allErrors, user, repeaterDefinition, metadataPath + "/" + entryName + "/", editActionId);
3119                        
3120                        // This entry does not exist anymore
3121                        repeaterMetadata.removeMetadata(entryName);
3122                    }
3123                    else
3124                    {
3125                        int newPosition = repeaterEntry.getPosition();
3126                        
3127                        // Check if position has changed
3128                        if (newPosition != currentPosition)
3129                        {
3130                            String finalEntryName = String.valueOf(newPosition);
3131                            String tempEntryName = finalEntryName;
3132                            
3133                            // Check if there is already an entry by that name
3134                            if (!repeaterMetadata.hasMetadata(String.valueOf(newPosition)))
3135                            {
3136                                // Move the composite to the new name
3137                                _moveRepeaterEntry(repeaterMetadata, entryName, finalEntryName);
3138                            }
3139                            else
3140                            {
3141                                // Move to a temporary name
3142                                tempEntryName = "temp-" + String.valueOf(newPosition);
3143                                _moveRepeaterEntry(repeaterMetadata, entryName, tempEntryName);
3144                                // Keep in mind for later move
3145                                laterMoves.put(tempEntryName, finalEntryName);
3146                            }
3147                        }
3148                    }
3149                }
3150            }
3151        }
3152        
3153        // Process moves
3154        for (Map.Entry<String, String> move : laterMoves.entrySet())
3155        {
3156            // Move to a temporary name
3157            _moveRepeaterEntry(repeaterMetadata, move.getKey(), move.getValue());
3158        }
3159
3160        for (String entryName : repeaterMetadata.getMetadataNames())
3161        {
3162            // Only process composite metadata
3163            if (repeaterMetadata.getType(entryName) == org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType.COMPOSITE)
3164            {
3165                int currentPosition = NumberUtils.toInt(entryName, -1);
3166
3167                if (currentPosition != -1)
3168                {
3169                    RepeaterEntry repeaterEntry = _getCurrentEntry(entries, entryName);
3170                    
3171                    if (repeaterEntry != null)
3172                    {
3173                        // Synchronization
3174                        ModifiableCompositeMetadata entryMetadata = repeaterMetadata.getCompositeMetadata(entryName, true);
3175                        _synchronizeMetadataSetElement(content, entryMetadata, repeaterEntry, allErrors, user, metadataSetElement, repeaterDefinition, metadataPath + "/" + entryName + "/", editActionId, externalAndLocalMetadata);
3176                    }
3177                }
3178            }
3179        }
3180        
3181        // Creates new entries
3182        for (RepeaterEntry repeaterEntry : entries)
3183        {
3184            if (repeaterEntry.getPreviousPosition() == -1)
3185            {
3186                int position = repeaterEntry.getPosition();
3187                // New entry
3188                ModifiableCompositeMetadata entryMetadata = repeaterMetadata.getCompositeMetadata(String.valueOf(position), true);
3189                _synchronizeMetadataSetElement(content, entryMetadata, repeaterEntry, allErrors, user, metadataSetElement, repeaterDefinition, metadataPath + "/" + position + "/", editActionId, externalAndLocalMetadata);
3190            }
3191        }
3192    }
3193    
3194    /**
3195     * Do additional processing to remove entry sub-metadatas.
3196     * @param content the processed content.
3197     * @param metadata the composite metadata being removed.
3198     * @param form the form.
3199     * @param allErrors the errors.
3200     * @param user the user.
3201     * @param parentMetadataDefinition the parent metadata definition.
3202     * @param metadataPath the metadata path.
3203     * @param editActionId The current 'edit content' action ID.
3204     * @throws WorkflowException if an error occurs.
3205     */
3206    protected void _synchronizeMetadataRemoval(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, MetadataDefinition parentMetadataDefinition, String metadataPath, int editActionId) throws WorkflowException
3207    {
3208        // Currently only purpose is to handle
3209        if (_invertRelationEnabled())
3210        {
3211            // Use the real metadatas instead of the metadata-set, because they will be physically removed
3212            // even if they aren't in the metadata-set.
3213            for (String subMetadataName : parentMetadataDefinition.getMetadataNames())
3214            {
3215                MetadataDefinition metadataDefinition = parentMetadataDefinition.getMetadataDefinition(subMetadataName);
3216                if (metadataDefinition != null)
3217                {
3218                    String subMetadataPath = metadataPath + subMetadataName;
3219                    if (metadata.hasMetadata(subMetadataName))
3220                    {
3221                        if (metadataDefinition.getType() == MetadataType.COMPOSITE)
3222                        {
3223                            // Recurse in composites.
3224                            ModifiableCompositeMetadata compositeMetadata = metadata.getCompositeMetadata(subMetadataName);
3225                            _synchronizeMetadataRemoval(content, compositeMetadata, form, allErrors, user, metadataDefinition, subMetadataPath + "/", editActionId);
3226                        }
3227                        else if (metadataDefinition.getType() == MetadataType.CONTENT)
3228                        {
3229                            // In case of existing mutual relationship, remove it.
3230                            String[] refContentIds = metadata.getStringArray(subMetadataName, new String[0]);
3231                            _removeInvertRelations(content.getId(), metadataDefinition, subMetadataPath, refContentIds, editActionId, allErrors);
3232                        }
3233                    }
3234                }
3235            }
3236        }
3237    }
3238    
3239    /**
3240     * Move a repeater entry.
3241     * @param metadata the parent composite metadata.
3242     * @param fromName the current entry name.
3243     * @param toName the new entry name.
3244     * @throws WorkflowException if an error occurs.
3245     */
3246    protected void _moveRepeaterEntry(CompositeMetadata metadata, String fromName, String toName) throws WorkflowException
3247    {
3248        // FIXME movablemetadata
3249        if (!(metadata instanceof JCRCompositeMetadata))
3250        {
3251            throw new WorkflowException("Unable to manage non JCR composite metadata: " + metadata);
3252        }
3253        
3254        try
3255        {
3256            Node node = ((JCRCompositeMetadata) metadata).getNode();
3257            node.getSession().move(node.getNode(JCRCompositeMetadata.METADATA_PREFIX + fromName).getPath(),
3258                                   node.getPath() + "/" + JCRCompositeMetadata.METADATA_PREFIX + toName);
3259        }
3260        catch (RepositoryException e)
3261        {
3262            throw new WorkflowException("Unable to move repeater entry", e);
3263        }
3264    }
3265
3266    /**
3267     * Retrieves a repeater entry corresponding to an entry name.
3268     * @param entries the entries.
3269     * @param entryName the entry name.
3270     * @return the entry found or <code>null</code> otherwise.
3271     */
3272    protected RepeaterEntry _getEntry(List<RepeaterEntry> entries, String entryName)
3273    {
3274        int position = Integer.valueOf(entryName);
3275        
3276        for (RepeaterEntry entry : entries)
3277        {
3278            int previousPosition = entry.getPreviousPosition();
3279            
3280            if (previousPosition != -1 && previousPosition == position)
3281            {
3282                return entry;
3283            }
3284        }
3285        
3286        // Not found
3287        return null;
3288    }
3289
3290    /**
3291     * Retrieves a repeater entry corresponding to an entry name.
3292     * @param entries the entries.
3293     * @param entryName the entry name.
3294     * @return the entry found or <code>null</code> otherwise.
3295     */
3296    protected RepeaterEntry _getCurrentEntry(List<RepeaterEntry> entries, String entryName)
3297    {
3298        int seekPosition = Integer.valueOf(entryName);
3299        
3300        for (RepeaterEntry entry : entries)
3301        {
3302            int position = entry.getPosition();
3303            
3304            if (position != -1 && position == seekPosition)
3305            {
3306                return entry;
3307            }
3308        }
3309        
3310        // Not found
3311        return null;
3312    }
3313    
3314    /**
3315     * Synchronize a string metadata from a field.
3316     * @param metadata the metadata.
3317     * @param form the form containing the field.
3318     * @param metadataDefinition the metadata definition.
3319     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
3320     * @throws WorkflowException if an error occurs.
3321     */
3322    protected void _synchronizeStringMetadata(ModifiableCompositeMetadata metadata, Form form, MetadataDefinition metadataDefinition, boolean externalizable) throws WorkflowException
3323    {
3324        String metadataName = metadataDefinition.getName();
3325        
3326        SimpleField<String> field = form.getStringArray(metadataName);
3327        
3328        if (field != null && field.getValues() != null && (metadataDefinition.isMultiple() || (field.getValues().length > 0 && field.getValues()[0] != null)))
3329        {
3330            if (metadataDefinition.isMultiple())
3331            {
3332                _synchronizeMultipleStringMetadata(metadata, form, externalizable, metadataName, field);
3333            }
3334            else 
3335            {
3336                if (field.getMode() == MODE.REMOVE)
3337                {
3338                    metadata.removeMetadata(metadataName);
3339                }
3340                else
3341                {
3342                    _setMetadata(metadata, metadataName, form, field.getValues()[0], externalizable);
3343                }
3344            }
3345        }
3346        else
3347        {
3348            if (externalizable)
3349            {
3350                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
3351                ExternalizableMetadataHelper.updateStatus(metadata, metadataName, status);
3352            }
3353            
3354            _removeMetadataIfExists(metadata, metadataName, externalizable);
3355        }
3356    }
3357
3358    private void _synchronizeMultipleStringMetadata(ModifiableCompositeMetadata metadata, Form form, boolean externalizable, String metadataName, SimpleField<String> field)
3359    {
3360        if (field.getMode() == MODE.REPLACE 
3361            || field.getMode() == MODE.INSERT && !metadata.hasMetadata(metadataName))
3362        {
3363            _setMetadata(metadata, metadataName, form, field.getValues(), externalizable);
3364        }
3365        else if (field.getMode() != MODE.REMOVE || metadata.hasMetadata(metadataName))
3366        {
3367            String[] array = metadata.getStringArray(metadataName);
3368            if (field.getMode() == MODE.INSERT)
3369            {
3370                array = ArrayUtils.addAll(array, field.getValues());
3371            }
3372            else
3373            {
3374                array = ArrayUtils.removeElements(array, field.getValues());
3375            }
3376            
3377            if (array.length > 0)
3378            {
3379                _setMetadata(metadata, metadataName, form, array, externalizable);
3380            }
3381            else
3382            {
3383                _removeMetadataIfExists(metadata, metadataName, externalizable);
3384            }
3385        }
3386    }
3387    
3388    /**
3389     * Synchronize a string metadata from a field.
3390     * @param metadata the metadata.
3391     * @param form the form containing the field.
3392     * @param metadataDefinition the metadata definition.
3393     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
3394     * @throws WorkflowException if an error occurs.
3395     */
3396    protected void _synchronizeMultilingualStringMetadata(ModifiableCompositeMetadata metadata, Form form, MetadataDefinition metadataDefinition, boolean externalizable) throws WorkflowException
3397    {
3398        String metadataName = metadataDefinition.getName();
3399        
3400        SimpleField<MultilingualString> field = form.getMultilingualStringArray(metadataName);
3401        
3402        if (field != null && field.getValues() != null && (field.getValues().length > 0 && field.getValues()[0] != null))
3403        {
3404            if (field.getMode() == MODE.REMOVE)
3405            {
3406                metadata.removeMetadata(metadataName);
3407            }
3408            else
3409            {
3410                _setMetadata(metadata, metadataName, form, field.getValues()[0], externalizable);
3411            }
3412        }
3413        else
3414        {
3415            if (externalizable)
3416            {
3417                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
3418                ExternalizableMetadataHelper.updateStatus(metadata, metadataName, status);
3419            }
3420            
3421            _removeMetadataIfExists(metadata, metadataName, externalizable);
3422        }
3423    }
3424    
3425    /**
3426     * Synchronize a user metadata from a field.
3427     * @param metadata the metadata.
3428     * @param form the form containing the field.
3429     * @param metadataDefinition the metadata definition.
3430     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
3431     * @throws WorkflowException if an error occurs.
3432     */
3433    protected void _synchronizeUserMetadata(ModifiableCompositeMetadata metadata, Form form, MetadataDefinition metadataDefinition, boolean externalizable) throws WorkflowException
3434    {
3435        String metadataName = metadataDefinition.getName();
3436        SimpleField<UserIdentity> field = form.getUserArray(metadataName);
3437        
3438        if (field != null && field.getValues() != null && (metadataDefinition.isMultiple() || (field.getValues().length > 0 && field.getValues()[0] != null)))
3439        {
3440            if (metadataDefinition.isMultiple())
3441            {
3442                _synchronizeMultipleUserMetadata(metadata, form, externalizable, metadataName, field);
3443            }
3444            else 
3445            {
3446                if (field.getMode() == MODE.REMOVE)
3447                {
3448                    metadata.removeMetadata(metadataName);
3449                }
3450                else
3451                {
3452                    _setMetadata(metadata, metadataName, form, field.getValues()[0], externalizable);
3453                }
3454            }
3455        }
3456        else
3457        {
3458            if (externalizable)
3459            {
3460                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
3461                ExternalizableMetadataHelper.updateCompositeMetadataStatus(metadata, metadataName, status);
3462            }
3463            
3464            _removeMetadataIfExists(metadata, metadataName, externalizable);
3465        }
3466    }
3467
3468    private void _synchronizeMultipleUserMetadata(ModifiableCompositeMetadata metadata, Form form, boolean externalizable, String metadataName, SimpleField<UserIdentity> field)
3469    {
3470        if (field.getMode() == MODE.REPLACE 
3471            || field.getMode() == MODE.INSERT && !metadata.hasMetadata(metadataName))
3472        {
3473            _setMetadata(metadata, metadataName, form, field.getValues(), externalizable);
3474        }
3475        else if (field.getMode() != MODE.REMOVE || metadata.hasMetadata(metadataName))
3476        {
3477            UserIdentity[] array = metadata.getUserArray(metadataName);
3478            if (field.getMode() == MODE.INSERT)
3479            {
3480                array = ArrayUtils.addAll(array, field.getValues());
3481            }
3482            else
3483            {
3484                array = ArrayUtils.removeElements(array, field.getValues());
3485            }
3486            
3487            if (array.length > 0)
3488            {
3489                _setMetadata(metadata, metadataName, form, array, externalizable);
3490            }
3491            else
3492            {
3493                _removeMetadataIfExists(metadata, metadataName, externalizable);
3494            }
3495        }
3496    }
3497
3498    /**
3499     * Synchronize a date metadata from a field.
3500     * @param metadata the metadata.
3501     * @param form the form containing the field.
3502     * @param metadataDefinition the metadata definition.
3503     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
3504     * @throws WorkflowException if an error occurs.
3505     */
3506    protected void _synchronizeDateMetadata(ModifiableCompositeMetadata metadata, Form form, MetadataDefinition metadataDefinition, boolean externalizable) throws WorkflowException
3507    {
3508        String metadataName = metadataDefinition.getName();
3509        SimpleField<Date> field = form.getDateArray(metadataName);
3510        
3511        if (field != null && field.getValues() != null && (metadataDefinition.isMultiple() || (field.getValues().length > 0 && field.getValues()[0] != null)))
3512        {
3513            if (metadataDefinition.isMultiple())
3514            {
3515                _synchronizeMultipleDateMetadata(metadata, form, externalizable, metadataName, field);
3516            }
3517            else 
3518            {
3519                if (field.getMode() == MODE.REMOVE)
3520                {
3521                    metadata.removeMetadata(metadataName);
3522                }
3523                else
3524                {
3525                    _setMetadata(metadata, metadataName, form, field.getValues()[0], externalizable);
3526                }
3527            }
3528        }
3529        else
3530        {
3531            if (externalizable)
3532            {
3533                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
3534                ExternalizableMetadataHelper.updateStatus(metadata, metadataName, status);
3535            }
3536            
3537            _removeMetadataIfExists(metadata, metadataName, externalizable);
3538        }
3539    }
3540
3541    private void _synchronizeMultipleDateMetadata(ModifiableCompositeMetadata metadata, Form form, boolean externalizable, String metadataName, SimpleField<Date> field)
3542    {
3543        if (field.getMode() == MODE.REPLACE 
3544            || field.getMode() == MODE.INSERT && !metadata.hasMetadata(metadataName))
3545        {
3546            _setMetadata(metadata, metadataName, form, field.getValues(), externalizable);
3547        }
3548        else if (field.getMode() != MODE.REMOVE || metadata.hasMetadata(metadataName))
3549        {
3550            Date[] array = metadata.getDateArray(metadataName);
3551            if (field.getMode() == MODE.INSERT)
3552            {
3553                array = ArrayUtils.addAll(array, field.getValues());
3554            }
3555            else
3556            {
3557                array = ArrayUtils.removeElements(array, field.getValues());
3558            }
3559
3560            if (array.length > 0)
3561            {
3562                _setMetadata(metadata, metadataName, form, array, externalizable);
3563            }
3564            else
3565            {
3566                _removeMetadataIfExists(metadata, metadataName, externalizable);
3567            }
3568        }
3569    }
3570    
3571    /**
3572     * Remove a metadata if exists.
3573     * Be careful ! If externalizable, this method have to be called after setting the current status.
3574     * @param metadata The parent composite metadata
3575     * @param metadataName The metadata name
3576     * @param externalizable <code>true</code> if externalizable
3577     */
3578    protected void _removeMetadataIfExists (ModifiableCompositeMetadata metadata, String metadataName, boolean externalizable)
3579    {
3580        if (externalizable)
3581        {
3582            ExternalizableMetadataHelper.removeLocalMetadataIfExists(metadata, metadataName);
3583        }
3584        else
3585        {
3586            if (metadata.hasMetadata(metadataName))
3587            {
3588                metadata.removeMetadata(metadataName);
3589            }
3590        }
3591    }
3592
3593    /**
3594     * Synchronize a long metadata from a field.
3595     * @param metadata the metadata.
3596     * @param form the form containing the field.
3597     * @param metadataDefinition the metadata definition.
3598     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
3599     * @throws WorkflowException if an error occurs.
3600     */
3601    protected void _synchronizeLongMetadata(ModifiableCompositeMetadata metadata, Form form, MetadataDefinition metadataDefinition, boolean externalizable) throws WorkflowException
3602    {
3603        String metadataName = metadataDefinition.getName();
3604        SimpleField<Long> field = form.getLongArray(metadataName);
3605        
3606        if (field != null && field.getValues() != null && (metadataDefinition.isMultiple() || (field.getValues().length > 0 && field.getValues()[0] != null)))
3607        {
3608            if (metadataDefinition.isMultiple())
3609            {
3610                _synchronizeMultipleLongMetadata(metadata, form, externalizable, metadataName, field);
3611            }
3612            else 
3613            {
3614                if (field.getMode() == MODE.REMOVE)
3615                {
3616                    metadata.removeMetadata(metadataName);
3617                }
3618                else
3619                {
3620                    _setMetadata(metadata, metadataName, form, field.getValues()[0], externalizable);
3621                }
3622            }
3623        }
3624        else
3625        {
3626            if (externalizable)
3627            {
3628                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
3629                ExternalizableMetadataHelper.updateStatus(metadata, metadataName, status);
3630            }
3631            
3632            _removeMetadataIfExists(metadata, metadataName, externalizable);
3633        }
3634    }
3635
3636    private void _synchronizeMultipleLongMetadata(ModifiableCompositeMetadata metadata, Form form, boolean externalizable, String metadataName, SimpleField<Long> field)
3637    {
3638        if (field.getMode() == MODE.REPLACE 
3639            || field.getMode() == MODE.INSERT && !metadata.hasMetadata(metadataName))
3640        {
3641            _setMetadata(metadata, metadataName, form, field.getValues(), externalizable);
3642        }
3643        else if (field.getMode() != MODE.REMOVE || metadata.hasMetadata(metadataName))
3644        {
3645            Long[] array = ArrayUtils.toObject(metadata.getLongArray(metadataName));
3646            if (field.getMode() == MODE.INSERT)
3647            {
3648                array = ArrayUtils.addAll(array, field.getValues());
3649            }
3650            else
3651            {
3652                array = ArrayUtils.removeElements(array, field.getValues());
3653            }
3654            
3655            if (array.length > 0)
3656            {
3657                _setMetadata(metadata, metadataName, form, array, externalizable);
3658            }
3659            else
3660            {
3661                _removeMetadataIfExists(metadata, metadataName, externalizable);
3662            }
3663        }
3664    }
3665    
3666    /**
3667     * Synchronize a geocode metadata from a field.
3668     * @param metadata the metadata.
3669     * @param form the form containing the field.
3670     * @param metadataDefinition the metadata definition.
3671     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
3672     * @throws WorkflowException if an error occurs.
3673     */
3674    protected void _synchronizeGeocodeMetadata(ModifiableCompositeMetadata metadata, Form form, MetadataDefinition metadataDefinition, boolean externalizable) throws WorkflowException
3675    {
3676        String metadataName = metadataDefinition.getName();
3677        SimpleField<Double> field = form.getDoubleArray(metadataName);
3678        
3679        if (field != null && field.getValues() != null && field.getMode() != MODE.REMOVE)
3680        {
3681            ModifiableCompositeMetadata geoCode = null;
3682            if (externalizable)
3683            {
3684                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
3685                geoCode = ExternalizableMetadataHelper.setLocalCompositeMetadata(metadata, metadataName, status);
3686            }
3687            else
3688            {
3689                geoCode = ExternalizableMetadataHelper.getCompositeMetadata(metadata, metadataName);
3690            }
3691            
3692            geoCode.setMetadata("longitude", field.getValues()[0]);                       
3693            geoCode.setMetadata("latitude", field.getValues()[1]);
3694        }
3695        else
3696        {
3697            if (externalizable)
3698            {
3699                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
3700                ExternalizableMetadataHelper.updateCompositeMetadataStatus(metadata, metadataName, status);
3701            }
3702            
3703            _removeMetadataIfExists(metadata, metadataName, externalizable);
3704        }
3705    }
3706
3707    /**
3708     * Synchronize a double metadata from a field.
3709     * @param metadata the metadata.
3710     * @param form the form containing the field.
3711     * @param metadataDefinition the metadata definition.
3712     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
3713     * @throws WorkflowException if an error occurs.
3714     */
3715    protected void _synchronizeDoubleMetadata(ModifiableCompositeMetadata metadata, Form form, MetadataDefinition metadataDefinition, boolean externalizable) throws WorkflowException
3716    {
3717        String metadataName = metadataDefinition.getName();
3718        SimpleField<Double> field = form.getDoubleArray(metadataName);
3719        
3720        if (field != null && field.getValues() != null && (metadataDefinition.isMultiple() || (field.getValues().length > 0 && field.getValues()[0] != null)))
3721        {
3722            if (metadataDefinition.isMultiple())
3723            {
3724                _synchronizeMultipleDoubleMetadata(metadata, form, externalizable, metadataName, field);
3725            }
3726            else 
3727            {
3728                if (field.getMode() == MODE.REMOVE)
3729                {
3730                    metadata.removeMetadata(metadataName);
3731                }
3732                else
3733                {
3734                    _setMetadata(metadata, metadataName, form, field.getValues()[0], externalizable);
3735                }
3736            }
3737        }
3738        else
3739        {
3740            if (externalizable)
3741            {
3742                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
3743                ExternalizableMetadataHelper.updateStatus(metadata, metadataName, status);
3744            }
3745            
3746            _removeMetadataIfExists(metadata, metadataName, externalizable);
3747        }
3748    }
3749
3750    private void _synchronizeMultipleDoubleMetadata(ModifiableCompositeMetadata metadata, Form form, boolean externalizable, String metadataName, SimpleField<Double> field)
3751    {
3752        if (field.getMode() == MODE.REPLACE 
3753            || field.getMode() == MODE.INSERT && !metadata.hasMetadata(metadataName))
3754        {
3755            _setMetadata(metadata, metadataName, form, field.getValues(), externalizable);
3756        }
3757        else if (field.getMode() != MODE.REMOVE || metadata.hasMetadata(metadataName))
3758        {
3759            Double[] array = ArrayUtils.toObject(metadata.getDoubleArray(metadataName));
3760            if (field.getMode() == MODE.INSERT)
3761            {
3762                array = ArrayUtils.addAll(array, field.getValues());
3763            }
3764            else
3765            {
3766                array = ArrayUtils.removeElements(array, field.getValues());
3767            }
3768            
3769            if (array.length > 0)
3770            {
3771                _setMetadata(metadata, metadataName, form, array, externalizable);
3772            }
3773            else
3774            {
3775                _removeMetadataIfExists(metadata, metadataName, externalizable);
3776            }
3777        }
3778    }
3779
3780    /**
3781     * Synchronize a boolean metadata from a field.
3782     * @param metadata the metadata.
3783     * @param form the form containing the field.
3784     * @param metadataDefinition the metadata definition.
3785     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
3786     * @throws WorkflowException if an error occurs.
3787     */
3788    protected void _synchronizeBooleanMetadata(ModifiableCompositeMetadata metadata, Form form, MetadataDefinition metadataDefinition, boolean externalizable) throws WorkflowException
3789    {
3790        String metadataName = metadataDefinition.getName();
3791        SimpleField<Boolean> field = form.getBooleanArray(metadataName);
3792        
3793        if (field != null && field.getValues() != null && (metadataDefinition.isMultiple() || (field.getValues().length > 0 && field.getValues()[0] != null)))
3794        {
3795            if (metadataDefinition.isMultiple())
3796            {
3797                _synchronizeMultipleBooleanMetadata(metadata, form, externalizable, metadataName, field);
3798            }
3799            else 
3800            {
3801                if (field.getMode() == MODE.REMOVE)
3802                {
3803                    metadata.removeMetadata(metadataName);
3804                }
3805                else
3806                {
3807                    _setMetadata(metadata, metadataName, form, field.getValues()[0], externalizable);
3808                }
3809            }
3810        }
3811        else
3812        {
3813            if (externalizable)
3814            {
3815                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
3816                ExternalizableMetadataHelper.updateStatus(metadata, metadataName, status);
3817            }
3818            
3819            _removeMetadataIfExists(metadata, metadataName, externalizable);
3820        }
3821    }
3822
3823    private void _synchronizeMultipleBooleanMetadata(ModifiableCompositeMetadata metadata, Form form, boolean externalizable, String metadataName, SimpleField<Boolean> field)
3824    {
3825        if (field.getMode() == MODE.REPLACE 
3826            || field.getMode() == MODE.INSERT && !metadata.hasMetadata(metadataName))
3827        {
3828            _setMetadata(metadata, metadataName, form, field.getValues(), externalizable);
3829        }
3830        else if (field.getMode() != MODE.REMOVE || metadata.hasMetadata(metadataName))
3831        {
3832            Boolean[] array = ArrayUtils.toObject(metadata.getBooleanArray(metadataName));
3833            if (field.getMode() == MODE.INSERT)
3834            {
3835                array = ArrayUtils.addAll(array, field.getValues());
3836            }
3837            else
3838            {
3839                array = ArrayUtils.removeElements(array, field.getValues());
3840            }
3841            
3842            if (array.length > 0)
3843            {
3844                _setMetadata(metadata, metadataName, form, array, externalizable);
3845            }
3846            else
3847            {
3848                _removeMetadataIfExists(metadata, metadataName, externalizable);
3849            }
3850        }
3851    }
3852
3853    /**
3854     * Synchronize a binary metadata from a field.
3855     * @param metadata the metadata.
3856     * @param form the form containing the field.
3857     * @param allErrors the errors.
3858     * @param user the user.
3859     * @param metadataDefinition the metadata definition.
3860     * @param metadataPath the current metadata path.
3861     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
3862     * @throws WorkflowException if an error occurs.
3863     */
3864    protected void _synchronizeBinaryMetadata(ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, MetadataDefinition metadataDefinition, String metadataPath, boolean externalizable) throws WorkflowException
3865    {
3866        String metadataName = metadataDefinition.getName();
3867        BinaryField field = form.getBinaryField(metadataName);
3868        
3869        if (field != null && field.getValues() != null)
3870        {
3871            String uploadId = field.getValues()[0];
3872            
3873            if (field.hasBinaryValue())
3874            {
3875                // Set the value from an existing metadata.
3876                BinaryMetadata binaryValue = field.getBinaryValue();
3877                
3878                ModifiableBinaryMetadata binaryMetadata = null;
3879                if (externalizable)
3880                {
3881                    ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
3882                    binaryMetadata = ExternalizableMetadataHelper.setLocalBinaryMetadata(metadata, metadataName, status);
3883                }
3884                else
3885                {
3886                    binaryMetadata = ExternalizableMetadataHelper.getBinaryMetadata(metadata, metadataName);
3887                }
3888                
3889                binaryMetadata.setFilename(binaryValue.getFilename());
3890                binaryMetadata.setMimeType(binaryValue.getMimeType());
3891                binaryMetadata.setEncoding(binaryValue.getEncoding());
3892                binaryMetadata.setLastModified(binaryValue.getLastModified());
3893                binaryMetadata.setInputStream(binaryValue.getInputStream());
3894            }
3895            else if (!uploadId.equals(UNTOUCHED_BINARY))
3896            {
3897                try
3898                {
3899                    Upload upload = _uploadManager.getUpload(user, uploadId);
3900                    ModifiableBinaryMetadata binaryMetadata = null;
3901                    if (externalizable)
3902                    {
3903                        ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
3904                        binaryMetadata = ExternalizableMetadataHelper.setLocalBinaryMetadata(metadata, metadataName, status);
3905                    }
3906                    else
3907                    {
3908                        binaryMetadata = ExternalizableMetadataHelper.getBinaryMetadata(metadata, metadataName);
3909                    }
3910                    
3911                    binaryMetadata.setFilename(upload.getFilename());
3912                    binaryMetadata.setMimeType(upload.getMimeType());
3913                    binaryMetadata.setLastModified(upload.getUploadedDate());
3914                    binaryMetadata.setInputStream(upload.getInputStream());
3915                }
3916                catch (NoSuchElementException e)
3917                {
3918                    Errors errors = new Errors();
3919                    
3920                    List<String> parameters = new ArrayList<>();
3921                    parameters.add(uploadId);
3922                    parameters.add(metadataPath);
3923                    errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_UPLOAD_MISSING", parameters));
3924                    allErrors.addError(metadataPath, errors);
3925                }
3926            }
3927        }
3928        else
3929        {
3930            if (externalizable)
3931            {
3932                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
3933                ExternalizableMetadataHelper.updateBinaryMetadataStatus(metadata, metadataName, status);
3934            }
3935            
3936            _removeMetadataIfExists(metadata, metadataName, externalizable);
3937        }
3938    }
3939    
3940    /**
3941     * Synchronize a file metadata from a field.
3942     * @param metadata the metadata.
3943     * @param form the form containing the field.
3944     * @param allErrors the errors.
3945     * @param user the user.
3946     * @param metadataDefinition the metadata definition.
3947     * @param metadataPath the current metadata path.
3948     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
3949     * @throws WorkflowException if an error occurs.
3950     */
3951    protected void _synchronizeFileMetadata(ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, MetadataDefinition metadataDefinition, String metadataPath, boolean externalizable) throws WorkflowException
3952    {
3953        String metadataName = metadataDefinition.getName();
3954        SimpleField<String> field = form.getStringArray(metadataName);
3955        
3956        if (field != null && field.getValues() != null)
3957        {
3958            String fileType = form.getStringArray(metadataName + "#type").getValues()[0];
3959            String localMetadataName = metadataName;
3960            if (externalizable)
3961            {
3962                localMetadataName = ExternalizableMetadataHelper.getMetadataName(metadata, metadataName, ExternalizableMetadataStatus.LOCAL);
3963            }
3964            
3965            if (METADATA_FILE.equals(fileType))
3966            {
3967                try
3968                {
3969                    // Remove string metadata if exists
3970                    metadata.getString(localMetadataName);
3971                    metadata.removeMetadata(localMetadataName);
3972                }
3973                catch (UnknownMetadataException e)
3974                {
3975                    // Do nothing
3976                }
3977                
3978                _synchronizeBinaryMetadata(metadata, form, allErrors, user, metadataDefinition, metadataPath, externalizable);
3979            }
3980            else if (EXPLORER_FILE.equals(fileType))
3981            {
3982                try
3983                {
3984                    metadata.getBinaryMetadata(localMetadataName, false);
3985                    metadata.removeMetadata(localMetadataName);
3986                }
3987                catch (UnknownMetadataException e)
3988                {
3989                    // Do nothing
3990                }
3991                
3992                _synchronizeStringMetadata(metadata, form, metadataDefinition, externalizable);
3993            }
3994        }
3995        else
3996        {
3997            if (externalizable)
3998            {
3999                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
4000                String localMetadataName = ExternalizableMetadataHelper.getMetadataName(metadata, metadataName, ExternalizableMetadataStatus.LOCAL);
4001                try
4002                {
4003                    metadata.getBinaryMetadata(localMetadataName, false);
4004                    ExternalizableMetadataHelper.updateBinaryMetadataStatus(metadata, metadataName, status);
4005                }
4006                catch (UnknownMetadataException e)
4007                {
4008                    // Do nothing
4009                }
4010                
4011                try
4012                {
4013                    metadata.getString(localMetadataName);
4014                    ExternalizableMetadataHelper.updateStatus(metadata, metadataName, status);
4015                }
4016                catch (UnknownMetadataException e)
4017                {
4018                    // Do nothing
4019                }
4020            }
4021            
4022            _removeMetadataIfExists(metadata, metadataName, externalizable);
4023        }
4024    }
4025    
4026    /**
4027     * Prepare to synchronize a content reference metadata from a field.
4028     * @param content the content.
4029     * @param metadata the metadata.
4030     * @param form the form containing the field.
4031     * @param allErrors the errors.
4032     * @param user the user.
4033     * @param metadataDefinition the metadata definition.
4034     * @param metadataPath the current metadata path.
4035     * @param invertEditActionId The action id for editing invert relation
4036     * @throws AmetysRepositoryException If an error occurs.
4037     * @throws WorkflowException if an error occurs.
4038     */
4039    protected void _prepareSynchronizeContentReferenceMetadata (Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, MetadataDefinition metadataDefinition, String metadataPath, int invertEditActionId) throws AmetysRepositoryException, WorkflowException
4040    {
4041        try
4042        {
4043            if (_invertRelationEnabled() && StringUtils.isNotEmpty(metadataDefinition.getInvertRelationPath()))
4044            {
4045                String metadataName = metadataDefinition.getName();
4046                SimpleField<Content> field = form.getContentArray(metadataName);
4047                
4048                if (field != null && field.getValues() != null)
4049                {
4050                    Content[] values = field.getValues();
4051                    
4052                    String[] contentIds = new String[values.length];
4053                    for (int i = 0; i < values.length; i++)
4054                    {
4055                        contentIds[i] = values[i].getId();
4056                    }
4057                    
4058                    String[] oldValues = metadata.getStringArray(metadataName, new String[0]);
4059                    String[] newRefValues = new String[0];
4060                    String[] toRemoveValues = new String[0];
4061                    
4062                    if (field.getMode() == MODE.REPLACE)
4063                    {
4064                        newRefValues = ArrayUtils.removeElements(contentIds, oldValues);
4065                        toRemoveValues = ArrayUtils.removeElements(oldValues, contentIds);
4066                    }
4067                    else if (field.getMode() == MODE.INSERT)
4068                    {
4069                        newRefValues = ArrayUtils.removeElements(contentIds, oldValues);
4070                        
4071                        if (!metadataDefinition.isMultiple())
4072                        {
4073                            toRemoveValues = oldValues; // the old value should be removed
4074                        }
4075                    }
4076                    else // (field.getMode() == MODE.REMOVE)
4077                    {
4078                        toRemoveValues = contentIds;
4079                    }
4080                    
4081                    // New mutual references which will be added
4082                    _lockNewReferencedContent(content, allErrors, user, metadataDefinition, metadataPath, invertEditActionId, newRefValues);
4083                    
4084                    // Old mutual references which will be removed
4085                    _lockOldReferencedThatWillBeRemovedContent(allErrors, user, metadataDefinition, metadataPath, invertEditActionId, toRemoveValues);
4086                }
4087                else if (metadata.hasMetadata(metadataName))
4088                {
4089                    _lockOldReferencedContent(metadata, allErrors, user, metadataDefinition, metadataPath, invertEditActionId, metadataName);
4090                }
4091            }
4092        }
4093        catch (RepositoryException e)
4094        {
4095            throw new WorkflowException("Error preparing to synchronize content references for content " + content.getId() + " and metadata at path '" + metadataPath + "'.", e);
4096        }
4097    }
4098
4099    private void _lockNewReferencedContent(Content content, AllErrors allErrors, UserIdentity user, MetadataDefinition metadataDefinition, String metadataPath, int invertEditActionId, String[] newValues) throws RepositoryException
4100    {
4101        for (String refContentId : newValues)
4102        {
4103            Content refContent = _resolver.resolveById(refContentId);
4104            if (_needTriggerEditWorkflowAction((ModifiableContent) refContent, metadataDefinition.getContentType(), metadataDefinition.getInvertRelationPath(), content.getId()))
4105            {
4106                // Check if edit action in available on referenced contents
4107                if (_isEditRefContentAvailable(invertEditActionId, refContent, metadataDefinition.getForceInvert(), metadataPath, user, allErrors))
4108                {
4109                    if (refContent instanceof LockableAmetysObject && !((LockableAmetysObject) refContent).isLocked())
4110                    {
4111                        // Get lock on referenced content
4112                        ((LockableAmetysObject) refContent).lock();
4113                    }
4114                }
4115            }
4116        }
4117    }
4118
4119    private void _lockOldReferencedThatWillBeRemovedContent(AllErrors allErrors, UserIdentity user, MetadataDefinition metadataDefinition, String metadataPath, int invertEditActionId, String[] toRemove)
4120    {
4121        for (String refContentId : toRemove)
4122        {
4123            try
4124            {
4125                Content refContent = _resolver.resolveById(refContentId);
4126                if (_isEditRefContentAvailable(invertEditActionId, refContent, metadataDefinition.getForceInvert(), metadataPath, user, allErrors))
4127                {
4128                    if (refContent instanceof LockableAmetysObject && !((LockableAmetysObject) refContent).isLocked())
4129                    {
4130                        // Get lock on old referenced content
4131                        ((LockableAmetysObject) refContent).lock();
4132                    }
4133                }
4134            }
4135            catch (UnknownAmetysObjectException e)
4136            {
4137                // The old referenced content does not exist anymore. Ignore it.
4138            }
4139        }
4140    }
4141
4142    private void _lockOldReferencedContent(ModifiableCompositeMetadata metadata, AllErrors allErrors, UserIdentity user, MetadataDefinition metadataDefinition, String metadataPath, int invertEditActionId, String metadataName)
4143    {
4144        String[] oldValues = metadata.getStringArray(metadataName, new String[0]);
4145        for (String refContentId : oldValues)
4146        {
4147            try
4148            {
4149                Content refContent = _resolver.resolveById(refContentId);
4150                if (_isEditRefContentAvailable(invertEditActionId, refContent, metadataDefinition.getForceInvert(), metadataPath, user, allErrors))
4151                {
4152                    if (refContent instanceof LockableAmetysObject && !((LockableAmetysObject) refContent).isLocked())
4153                    {
4154                        // Get lock on old referenced content
4155                        ((LockableAmetysObject) refContent).lock();
4156                    }
4157                }
4158            }
4159            catch (UnknownAmetysObjectException e)
4160            {
4161                // The old referenced content does not exist anymore. Ignore it.
4162            }
4163        }
4164    }
4165    
4166    /**
4167     * Synchronize a content reference metadata from a field.
4168     * @param content the content.
4169     * @param metadata the metadata.
4170     * @param form the form containing the field.
4171     * @param allErrors the errors.
4172     * @param user the user.
4173     * @param metadataDefinition the metadata definition.
4174     * @param metadataPath the current metadata path.
4175     * @param invertEditActionId The action id for editing invert relation
4176     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
4177     * @throws WorkflowException if an error occurs.
4178     */
4179    protected void _synchronizeContentReferenceMetadata(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, MetadataDefinition metadataDefinition, String metadataPath, int invertEditActionId, boolean externalizable) throws WorkflowException
4180    {
4181        String metadataName = metadataDefinition.getName();
4182        SimpleField<Content> field = form.getContentArray(metadataName);
4183        String contentTypeId = metadataDefinition.getContentType();
4184        String invert = metadataDefinition.getInvertRelationPath();
4185        
4186        if (field != null && field.getValues() != null)
4187        {
4188            Content[] values = field.getValues();
4189            
4190            if (metadataDefinition.isMultiple())
4191            {
4192                _synchronizeMultipleContentReferenceMetadata(content, metadata, form, allErrors, field, metadataDefinition, metadataPath, invertEditActionId, contentTypeId, invert, externalizable);
4193            }
4194            else if (values.length > 0)
4195            {
4196                _synchronizeSingleContentReferenceMetadata(content, metadata, form, allErrors, field, metadataDefinition, metadataPath, invertEditActionId, contentTypeId, invert, externalizable);
4197            }
4198            else
4199            {
4200                if (externalizable)
4201                {
4202                    ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
4203                    ExternalizableMetadataHelper.updateStatus(metadata, metadataName, status);
4204                }
4205                
4206                _removeMetadataIfExists(metadata, metadataName, externalizable);
4207            }
4208        }
4209        else
4210        {
4211            ExternalizableMetadataStatus status = null;
4212            
4213            String[] oldValues = new String[0];
4214            if (externalizable)
4215            {
4216                status = form.getExternalizableField(metadataName).getStatus();
4217                ExternalizableMetadataHelper.updateStatus(metadata, metadataName, status);
4218                
4219                try
4220                {
4221                    oldValues = ExternalizableMetadataHelper.getStringArray(metadata, metadataName, status);
4222                }
4223                catch (UnknownMetadataException e)
4224                {
4225                    // Nothing
4226                }
4227            }
4228            else
4229            {
4230                oldValues = metadata.getStringArray(metadataName, new String[0]);
4231            }
4232            
4233            // Remove metadata and JCR references
4234            _removeMetadataIfExists(metadata, metadataName, externalizable);
4235            
4236            if (_invertRelationEnabled() && StringUtils.isNotEmpty(invert) && (!externalizable || ExternalizableMetadataStatus.LOCAL == form.getExternalizableField(metadataName).getStatus()))
4237            {
4238                _removeInvertRelations(content.getId(), metadataDefinition, metadataPath, oldValues, invertEditActionId, allErrors);
4239            }
4240        }
4241    }
4242    
4243    private void _synchronizeMultipleContentReferenceMetadata(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, SimpleField<Content> field, MetadataDefinition metadataDefinition, String metadataPath, int invertEditActionId, String contentTypeId, String invert, boolean externalizable) throws WorkflowException
4244    {
4245        String metadataName = metadataDefinition.getName();
4246        Content[] values = field.getValues();
4247        
4248        String[] contentIds = new String[values.length];
4249        for (int i = 0; i < values.length; i++)
4250        {
4251            contentIds[i] = values[i].getId();
4252        }
4253        
4254        String localMetadataName = ExternalizableMetadataHelper.getMetadataName(metadata, metadataName, ExternalizableMetadataStatus.LOCAL);
4255        String[] oldValues = metadata.getStringArray(localMetadataName, new String[0]);
4256        String[] newValues;
4257        
4258        if (field.getMode() == MODE.REPLACE 
4259            || field.getMode() == MODE.INSERT &&  !metadata.hasMetadata(metadataName))
4260        {
4261            newValues = contentIds;
4262            
4263            // Set content ID values.
4264            _setMetadata(metadata, metadataName, form, contentIds, externalizable);
4265        }
4266        else if (field.getMode() != MODE.REMOVE || metadata.hasMetadata(metadataName))
4267        {
4268            Set<String> newMetadataValues = new LinkedHashSet<>();
4269            newMetadataValues.addAll(Arrays.asList(oldValues));
4270            if (field.getMode() == MODE.INSERT)
4271            {
4272                newMetadataValues.addAll(Arrays.asList(contentIds));
4273            }
4274            else
4275            {
4276                newMetadataValues.removeAll(Arrays.asList(contentIds));
4277            }
4278            
4279            newValues = newMetadataValues.toArray(new String[0]);
4280            
4281            // Set content ID values.
4282            if (newValues.length > 0)
4283            {
4284                _setMetadata(metadata, metadataName, form, newValues, externalizable);
4285            }
4286            else
4287            {
4288                metadata.removeMetadata(metadataName);
4289            }
4290        }
4291        else
4292        {
4293            newValues = new String[0];
4294        }
4295        
4296        _synchronizeRelationOfMultipleContentReferenceMetadata(content, form, allErrors, metadataDefinition, metadataPath, invertEditActionId, contentTypeId, invert, externalizable, metadataName, values, oldValues, newValues);
4297    }
4298
4299    private void _synchronizeRelationOfMultipleContentReferenceMetadata(Content content, Form form, AllErrors allErrors, MetadataDefinition metadataDefinition, String metadataPath, int invertEditActionId, String contentTypeId, String invert, boolean externalizable, String metadataName, Content[] values, String[] oldValues, String[] newValues) throws WorkflowException
4300    {
4301        if (_invertRelationEnabled() && StringUtils.isNotEmpty(invert) && (!externalizable || ExternalizableMetadataStatus.LOCAL == form.getExternalizableField(metadataName).getStatus()))
4302        {
4303            // Remove old mutual references.
4304            String[] toRemove = ArrayUtils.removeElements(oldValues, newValues);
4305            _removeInvertRelations(content.getId(), metadataDefinition, metadataPath, toRemove, invertEditActionId, allErrors);
4306            
4307            // Content ID?
4308            for (Content refContent : values)
4309            {
4310                if (ArrayUtils.contains(newValues, refContent.getId()))
4311                {
4312                    _setInvertRelation(content, metadataPath, refContent, contentTypeId, invert, invertEditActionId, allErrors);
4313                }
4314            }
4315        }
4316    }
4317    
4318    private void _synchronizeSingleContentReferenceMetadata(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, SimpleField<Content> field, MetadataDefinition metadataDefinition, String metadataPath, int invertEditActionId, String contentTypeId, String invert, boolean externalizable) throws WorkflowException
4319    {
4320        String metadataName = metadataDefinition.getName();
4321        Content[] values = field.getValues();
4322        
4323        String oldValue = null;
4324        try
4325        {
4326            oldValue = externalizable ? ExternalizableMetadataHelper.getString(metadata, metadataName, ExternalizableMetadataStatus.LOCAL) : metadata.getString(metadataName, null);
4327        }
4328        catch (UnknownMetadataException e)
4329        {
4330            // nothing
4331        }
4332        
4333        Content refContent = values[0];
4334        boolean invertRequired = true;
4335        
4336        if (field.getMode() == MODE.REMOVE)
4337        {
4338            if (StringUtils.equals(refContent.getId(), oldValue))
4339            {
4340                ExternalizableMetadataStatus status = externalizable ? form.getExternalizableField(metadataName).getStatus() : null;
4341                
4342                _removeMetadataIfExists(metadata, metadataName, externalizable);
4343                
4344                if (externalizable)
4345                {
4346                    ExternalizableMetadataHelper.updateStatus(metadata, metadataName, status);
4347                }
4348            }
4349            else
4350            {
4351                invertRequired = false;
4352                if (_logger.isWarnEnabled())
4353                {
4354                    _logger.warn("Cannot remove reference to content '" + refContent.getId() + "' on content '" + content.getId() + "' metadata '" + metadataDefinition.getId() + "' because it is not a current value");
4355                }
4356            }
4357        }
4358        else
4359        {
4360            if (StringUtils.equals(refContent.getId(), oldValue))
4361            {
4362                invertRequired = false;
4363                if (externalizable)
4364                {
4365                    ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
4366                    ExternalizableMetadataHelper.updateStatus(metadata, metadataName, status);
4367                }
4368            }
4369            else
4370            {
4371                _setMetadata(metadata, metadataName, form, refContent.getId(), externalizable);
4372            }
4373            
4374        }
4375        
4376        // Do not edit the invert relation if the externalizable status is external
4377        invertRequired = invertRequired && (!externalizable || ExternalizableMetadataStatus.LOCAL == form.getExternalizableField(metadataName).getStatus());
4378        _synchronizeRelationOfSingleContentReferenceMetadata(content, allErrors, field, metadataDefinition, metadataPath, invertEditActionId, contentTypeId, invert, oldValue, refContent, invertRequired);
4379    }
4380
4381    private void _synchronizeRelationOfSingleContentReferenceMetadata(Content content, AllErrors allErrors, SimpleField<Content> field, MetadataDefinition metadataDefinition, String metadataPath, int invertEditActionId, String contentTypeId, String invert, String oldValue, Content refContent, boolean invertRequired) throws WorkflowException
4382    {
4383        if (_invertRelationEnabled() && invertRequired && StringUtils.isNotEmpty(invert))
4384        {
4385            if (oldValue != null)
4386            {
4387                _removeInvertRelation(content.getId(), metadataDefinition, metadataPath, oldValue, invertEditActionId, allErrors);
4388            }
4389        
4390            if (field.getMode() != MODE.REMOVE)
4391            {
4392                _setInvertRelation(content, metadataPath, refContent, contentTypeId, invert, invertEditActionId, allErrors);
4393            }
4394        }
4395    }
4396    
4397    /**
4398     * Synchronize a sub-content metadata from a field.
4399     * @param content The Content.
4400     * @param metadata the metadata.
4401     * @param form the form containing the field.
4402     * @param allErrors the errors.
4403     * @param user the user.
4404     * @param metadataDefinition the metadata definition.
4405     * @param metadataPath the current metadata path.
4406     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
4407     * @throws WorkflowException if an error occurs.
4408     */
4409    protected void _synchronizeSubContentMetadata(Content content, ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, MetadataDefinition metadataDefinition, String metadataPath, boolean externalizable) throws WorkflowException
4410    {
4411        String metadataName = metadataDefinition.getName();
4412        
4413        SubContentField subContentField = form.getSubContentField(metadataName);
4414        
4415        String[] values = subContentField.getContentNamesOrIds();
4416        String[] contentTypes = subContentField.getContentTypes();
4417        Boolean[] isNew = subContentField.getIsNew();
4418        
4419        String contentLanguage = subContentField.getContentLanguage();
4420        int initWorkflowActionId = subContentField.getWorkflowActionId();
4421        String workflowName = subContentField.getWorkflowName();
4422        
4423        // TODO Handle ordering.
4424        TraversableAmetysObject objectCollection = null;
4425        if (externalizable)
4426        {
4427            ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
4428            objectCollection = ExternalizableMetadataHelper.getObjectCollection(metadata, metadataName, status);
4429        }
4430        else
4431        {
4432            objectCollection = ExternalizableMetadataHelper.getObjectCollection(metadata, metadataName);
4433        }
4434        
4435        
4436        Map<String, ModifiableContent> contentsToDelete = new HashMap<>();
4437        
4438        if (subContentField.getMode() == MODE.REPLACE)
4439        {
4440            try (AmetysObjectIterable<Content> subContents = objectCollection.getChildren())
4441            {
4442                for (Content subContent : subContents)
4443                {
4444                    if (subContent instanceof ModifiableContent)
4445                    {
4446                        contentsToDelete.put(subContent.getId(), (ModifiableContent) subContent);
4447                    }
4448                }
4449            }
4450        }
4451        else if (subContentField.getMode() == MODE.REMOVE)
4452        {
4453            for (String value : values)
4454            {
4455                ModifiableContent contentToRemove = _resolver.resolveById(value);
4456                contentsToDelete.put(contentToRemove.getId(), contentToRemove);
4457            }            
4458        }
4459        
4460        if (values != null  && (metadataDefinition.isMultiple() || values[0] != null) && subContentField.getMode() != MODE.REMOVE)
4461        {
4462            try
4463            {
4464                for (int i = 0; i < values.length; i++)
4465                {
4466                    if (isNew[i])
4467                    {
4468                        // New content: create it.
4469                        AmetysObjectWorkflow workflow = _workflowProvider.getAmetysObjectWorkflow();
4470                        Map<String, Object> inputs = _getInputsForSubContentCreation(content, metadata, form, user, metadataDefinition, metadataPath, values[i], new String[] {contentTypes[i]}, contentLanguage);
4471                        workflow.initialize(workflowName, initWorkflowActionId, inputs);
4472                    }
4473                    else
4474                    {
4475                        // The content is still there: remove it from the list of contents to delete.
4476                        String contentId = values[i];
4477                        contentsToDelete.remove(contentId);
4478                    }
4479                }
4480                
4481            }
4482            catch (WorkflowException e)
4483            {
4484                Errors errors = new Errors();
4485                
4486                List<String> parameters = new ArrayList<>();
4487                parameters.add(metadataPath);
4488                parameters.add(content.getId());
4489                errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_SUBCONTENT_CREATION", parameters));
4490                allErrors.addError(metadataPath, errors);
4491            }
4492        }
4493        
4494        // Delete contents.
4495        for (ModifiableContent contentToDelete : contentsToDelete.values())
4496        {
4497            contentToDelete.remove();
4498        }
4499    }
4500    
4501
4502    /**
4503     * Provide the inputs to use for the creation of a subcontent.
4504     * @param content The parent content
4505     * @param metadata the metadata.
4506     * @param form the form containing the field.
4507     * @param user the user.
4508     * @param metadataDefinition the metadata definition.
4509     * @param metadataPath the current metadata path.
4510     * @param name The name of the subcontent to create
4511     * @param cTypes Content types array of the subcontent to create
4512     * @param language The language of the subcontent to create
4513     * @return the map of inputs
4514     */
4515    protected Map<String, Object> _getInputsForSubContentCreation(Content content, ModifiableCompositeMetadata metadata, Form form, UserIdentity user, MetadataDefinition metadataDefinition, String metadataPath, String name, String[] cTypes, String language)
4516    {
4517        Map<String, Object> inputs = new HashMap<>();
4518        Map<String, Object> result = new HashMap<>();
4519        
4520        inputs.put(CreateContentFunction.CONTENT_NAME_KEY, name);
4521        inputs.put(CreateContentFunction.CONTENT_TITLE_KEY, name);
4522        inputs.put(CreateContentFunction.CONTENT_TYPES_KEY, cTypes);
4523        inputs.put(CreateContentFunction.CONTENT_LANGUAGE_KEY, language);
4524        inputs.put(CreateContentFunction.PARENT_CONTENT_ID_KEY, content.getId());
4525        inputs.put(CreateContentFunction.PARENT_CONTENT_METADATA_PATH_KEY, metadataPath);
4526        
4527        inputs.put(AbstractWorkflowComponent.RESULT_MAP_KEY, result);
4528        inputs.put(AbstractWorkflowComponent.FAIL_CONDITIONS_KEY, new ArrayList<String>());
4529        
4530        return inputs;
4531    }
4532    
4533    
4534    /**
4535     * Synchronize a sub-content metadata from a field.
4536     * @param metadata the metadata.
4537     * @param form the form containing the field.
4538     * @param metadataDefinition the metadata definition.
4539     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
4540     */
4541    protected void _synchronizeReferenceMetadata(ModifiableCompositeMetadata metadata, Form form, MetadataDefinition metadataDefinition, boolean externalizable)
4542    {
4543        String metadataName = metadataDefinition.getName();
4544        ReferenceField field = form.getReferenceField(metadataName);
4545        
4546        if (field != null && field.getValue() != null && field.getMode() != MODE.REMOVE)
4547        {
4548            ModifiableCompositeMetadata refMetadata = null;
4549            if (externalizable)
4550            {
4551                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
4552                refMetadata = ExternalizableMetadataHelper.getCompositeMetadata(metadata, metadataName, status, true);
4553            }
4554            else
4555            {
4556                refMetadata = ExternalizableMetadataHelper.getCompositeMetadata(metadata, metadataName);          
4557            }
4558            refMetadata.setMetadata("value", field.getValue());   
4559            refMetadata.setMetadata("type", field.getType());
4560        }
4561        else
4562        {
4563            if (metadata.hasMetadata(metadataName))
4564            {
4565                metadata.removeMetadata(metadataName);
4566            }
4567        }
4568    }
4569    
4570    /**
4571     * Synchronize a rich text metadata from a field.
4572     * @param metadata the metadata.
4573     * @param form the form containing the field.
4574     * @param allErrors the errors.
4575     * @param user the user.
4576     * @param metadataDefinition the metadata definition.
4577     * @param metadataPath the current metadata path.
4578     * @param externalizable <code>true</code> if the metadata is externalizable (local and external value)
4579     * @throws WorkflowException if an error occurs.
4580     */
4581    protected void _synchronizeRichTextMetadata(ModifiableCompositeMetadata metadata, Form form, AllErrors allErrors, UserIdentity user, MetadataDefinition metadataDefinition, String metadataPath, boolean externalizable) throws WorkflowException
4582    {
4583        String metadataName = metadataDefinition.getName();
4584        RichTextField richTextField = form.getRichTextField(metadataName);
4585        
4586        if (richTextField != null && richTextField.getContent() != null)
4587        {
4588            String format = richTextField.getFormat();
4589            
4590            ModifiableCompositeMetadata metadataHolder = _getMetadataHolder(metadata, metadataName);
4591            ModifiableRichText richText = null;
4592            if (externalizable)
4593            {
4594                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
4595                richText = ExternalizableMetadataHelper.setLocalRichTextMetadata(metadataHolder, metadataName, status);
4596            }
4597            else
4598            {
4599                richText = ExternalizableMetadataHelper.getRichTextMetadata(metadataHolder, metadataName);
4600            }
4601            
4602            if ("docbook".equals(format))
4603            {
4604                _copyRichText(richTextField, richText, metadataDefinition, metadataPath, allErrors);
4605            }
4606            else
4607            {
4608                _transformRichText(richTextField, richText, metadataDefinition, metadataPath, allErrors);
4609            }
4610        }
4611        else
4612        {
4613            if (externalizable)
4614            {
4615                ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
4616                ExternalizableMetadataHelper.updateRichTextMetadataStatus(metadata, metadataName, status);
4617            }
4618            
4619            _removeMetadataIfExists(metadata, metadataName, externalizable);
4620        }
4621    }
4622
4623    /**
4624     * Set the rich text metadata from a source that is not docbook.
4625     * @param field the RichText form field. 
4626     * @param richText the rich text metadata to populate.
4627     * @param metadataDefinition the metadata definition.
4628     * @param metadataPath the metadata path.
4629     * @param allErrors the error list.
4630     */
4631    protected void _transformRichText(RichTextField field, ModifiableRichText richText, MetadataDefinition metadataDefinition, String metadataPath, AllErrors allErrors)
4632    {
4633        try
4634        {
4635            RichTextTransformer richTextTransformer = metadataDefinition.getRichTextTransformer();
4636            String content = field.getContent();
4637            
4638            richTextTransformer.transform(content, richText);
4639        }
4640        catch (IOException e)
4641        {
4642            if (_logger.isErrorEnabled())
4643            {
4644                _logger.error("Unable to transform rich text", e);
4645            }
4646            
4647            Errors errors = new Errors();
4648            List<String> parameters = new ArrayList<>();
4649            parameters.add(e.getMessage());
4650            errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_RICHTEXT_TRANSFORM", parameters));
4651            allErrors.addError(metadataPath, errors);
4652        }
4653    }
4654
4655    /**
4656     * Set a rich text metadata from an existing rich text metadata.
4657     * @param field the RichText form field. 
4658     * @param richText the rich text metadata to populate.
4659     * @param metadataDefinition the metadata definition.
4660     * @param metadataPath the metadata path.
4661     * @param allErrors the error list.
4662     */
4663    protected void _copyRichText(RichTextField field, ModifiableRichText richText, MetadataDefinition metadataDefinition, String metadataPath, AllErrors allErrors)
4664    {
4665        try
4666        {
4667            String content = field.getContent();
4668            
4669            ByteArrayInputStream is = new ByteArrayInputStream(content.getBytes("UTF-8"));
4670            richText.setEncoding("UTF-8");
4671            richText.setMimeType("application/xml");
4672            richText.setLastModified(new Date());
4673            richText.setInputStream(is);
4674            
4675            ModifiableFolder dataFolder = richText.getAdditionalDataFolder();
4676            Map<String, Resource> dataToCopy = field.getAdditionalData();
4677            if (dataToCopy != null)
4678            {
4679                for (String fileName : dataToCopy.keySet())
4680                {
4681                    Resource sourceFile = dataToCopy.get(fileName);
4682                    
4683                    if (dataFolder.hasFile(fileName))
4684                    {
4685                        dataFolder.remove(fileName);
4686                    }
4687                    
4688                    ModifiableResource destFile = dataFolder.addFile(fileName).getResource();
4689                    destFile.setEncoding(sourceFile.getEncoding());
4690                    destFile.setMimeType(sourceFile.getMimeType());
4691                    destFile.setLastModified(sourceFile.getLastModified());
4692                    destFile.setInputStream(sourceFile.getInputStream());
4693                }
4694            }
4695        }
4696        catch (UnsupportedEncodingException e)
4697        {
4698            // Ignore
4699        }
4700    }
4701        
4702    /**
4703     * Determines if the edit workflow action will be trigger on this referenced content
4704     * @param refContent The referenced content
4705     * @param refContentTypeId The content type
4706     * @param invertRelationPath The path of invert relationship
4707     * @param currentContentId The content being editing
4708     * @return true if the edit workflow action will occur
4709     * @throws RepositoryException if an error occurred
4710     */
4711    protected boolean _needTriggerEditWorkflowAction (ModifiableContent refContent, String refContentTypeId, String invertRelationPath, String currentContentId) throws RepositoryException
4712    {
4713        ContentType refContentType = _contentTypeExtensionPoint.getExtension(refContentTypeId);
4714        MetadataDefinition invertMetadataDef = refContentType.getMetadataDefinitionByPath(invertRelationPath);
4715        
4716        ModifiableCompositeMetadata refContentMeta = _getRelationMetaHolder(refContent, refContentType, invertRelationPath, currentContentId, false);
4717        if (refContentMeta == null)
4718        {
4719            return true;
4720        }
4721        
4722        int lastSlashIndex = invertRelationPath.lastIndexOf(ContentConstants.METADATA_PATH_SEPARATOR);
4723        String metaName = lastSlashIndex > -1 ? invertRelationPath.substring(lastSlashIndex + 1) : invertRelationPath;
4724        
4725        if (invertMetadataDef.isMultiple())
4726        {
4727            String[] values = refContentMeta.getStringArray(metaName, new String[0]);
4728            if (!ArrayUtils.contains(values, currentContentId))
4729            {
4730                return true;
4731            }
4732            return false;
4733        }
4734        else
4735        {
4736            return !currentContentId.equals(refContentMeta.getString(metaName, null));
4737        }
4738    }
4739    
4740    /**
4741     * Set a mutual relation.
4742     * @param content the content being modified.
4743     * @param currentMetadataPath the metadata path on the content being modified.
4744     * @param refContent the content being referenced.
4745     * @param refContentTypeId the content type of the content being referenced.
4746     * @param invertRelationPath the path of the metadata to set on the content being referenced, separated by '/'
4747     * @param editActionId The current 'edit content' action ID.
4748     * @param allErrors the errors.
4749     * @throws WorkflowException if a fatal error occurs.
4750     */
4751    protected void _setInvertRelation(Content content, String currentMetadataPath, Content refContent, String refContentTypeId, String invertRelationPath, int editActionId, AllErrors allErrors) throws WorkflowException
4752    {
4753        try
4754        {
4755            boolean needSave = false;
4756            if (content instanceof ModifiableContent && invertRelationPath != null)
4757            {
4758                String currentContentId = content.getId();
4759                ModifiableContent modifiableRefContent = (ModifiableContent) refContent;
4760                
4761                ContentType refContentType = _contentTypeExtensionPoint.getExtension(refContentTypeId);
4762                MetadataDefinition invertMetadataDef = refContentType.getMetadataDefinitionByPath(invertRelationPath);
4763                
4764                Set<String> refExternalAndLocalMetadata = _externalizableMetadataProviderEP.getExternalAndLocalMetadata(modifiableRefContent);
4765                boolean externalizable = refExternalAndLocalMetadata.contains(invertRelationPath);
4766                        
4767                ModifiableCompositeMetadata refContentMeta = _getRelationMetaHolder(modifiableRefContent, refContentType, invertRelationPath, currentContentId, true);
4768                
4769                int lastSlashIndex = invertRelationPath.lastIndexOf(ContentConstants.METADATA_PATH_SEPARATOR);
4770                String metaName = lastSlashIndex > -1 ? invertRelationPath.substring(lastSlashIndex + 1) : invertRelationPath;
4771                
4772                if (invertMetadataDef.isMultiple())
4773                {
4774                    // Get the current values and add this one.
4775                    String[] values = refContentMeta.getStringArray(metaName, new String[0]);
4776                    if (!ArrayUtils.contains(values, currentContentId))
4777                    {
4778                        String[] newValues = ArrayUtils.add(values, currentContentId);
4779                        
4780                        if (externalizable)
4781                        {
4782                            // Force status to local
4783                            ExternalizableMetadataHelper.setLocalMetadata(refContentMeta, metaName, newValues, ExternalizableMetadataStatus.LOCAL);
4784                        }
4785                        else
4786                        {
4787                            ExternalizableMetadataHelper.setMetadata(refContentMeta, metaName, newValues);
4788                        }
4789                        needSave = true;
4790                    }
4791                }
4792                else
4793                {
4794                    // Remove the invert relation if it exists.
4795                    String oldContentId = refContentMeta.getString(metaName, null);
4796                    if (oldContentId != null && !currentContentId.equals(oldContentId))
4797                    {
4798                        _removeInvertRelation(refContent.getId(), invertMetadataDef, invertRelationPath, oldContentId, editActionId, allErrors);
4799                    }
4800                    
4801                    if (externalizable)
4802                    {
4803                        // Force status to local
4804                        ExternalizableMetadataHelper.setLocalMetadata(refContentMeta, metaName, currentContentId, ExternalizableMetadataStatus.LOCAL);
4805                    }
4806                    else
4807                    {
4808                        ExternalizableMetadataHelper.setMetadata(refContentMeta, metaName, currentContentId);
4809                    }
4810                    needSave = true;
4811                }
4812                
4813                if (needSave)
4814                {
4815                    modifiableRefContent.saveChanges();
4816                    
4817                    // Trigger edit workflow action on the referenced content if needed.
4818                    _triggerEditWorkflowAction(modifiableRefContent, editActionId);
4819                }
4820            }
4821        }
4822        catch (RepositoryException e)
4823        {
4824            _logger.error(String.format("Content reference validation error for content '%s', id '%s'", _contentHelper.getTitle(refContent), refContent.getId()), e);
4825            
4826            Errors errors = new Errors();
4827            Map<String, I18nizableText> params = new HashMap<>();
4828            params.put("content", new I18nizableText(_contentHelper.getTitle(refContent)));
4829            params.put("error", new I18nizableText(e.getLocalizedMessage()));
4830            errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_MUTUALRELATION_CREATE", params));
4831            allErrors.addError(currentMetadataPath, errors);
4832        }
4833    }
4834    
4835    /**
4836     * Check the lock status of the node
4837     * @param node The node
4838     * @throws RepositoryException If an error occurred while manipulating the node
4839     */
4840    protected void _checkLock(Node node) throws RepositoryException
4841    {
4842        if (!_lockAlreadyChecked.contains(node.getIdentifier()) && node.isLocked())
4843        {
4844            LockManager lockManager = node.getSession().getWorkspace().getLockManager();
4845            
4846            Lock lock = lockManager.getLock(node.getPath());
4847            Node lockHolder = lock.getNode();
4848            
4849            lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString());
4850            _lockAlreadyChecked.add(node.getIdentifier());
4851        }
4852    }
4853    
4854    /**
4855     * Checks if the "edit content" workflow action is available on a referenced content
4856     * @param editActionId The id of edit workflow action
4857     * @param refContent the referenced content to edit
4858     * @param forceInvert Force the invert edition regardless of the user's rights
4859     * @param currentMetadataPath the path of the metadata responsible for the invert relation
4860     * @param user the current user
4861     * @param allErrors the errors
4862     * @return <code>true</code> if the edit action is avalailable
4863     */
4864    protected boolean _isEditRefContentAvailable (int editActionId, Content refContent, boolean forceInvert, String currentMetadataPath, UserIdentity user, AllErrors allErrors)
4865    {
4866        if (refContent instanceof WorkflowAwareContent)
4867        {
4868            Map<String, Object> inputs = new HashMap<>();
4869            if (forceInvert)
4870            {
4871                // do not check user's right
4872                inputs.put(CheckRightsCondition.FORCE, true);
4873            }
4874            
4875            int[] availableActions = _workflowHelper.getAvailableActions((WorkflowAwareContent) refContent, inputs);
4876            if (!ArrayUtils.contains(availableActions, editActionId))
4877            {
4878                Errors errors = new Errors();
4879                Map<String, I18nizableText> params = new HashMap<>();
4880                
4881                // Check lock
4882                if (refContent instanceof LockableAmetysObject)
4883                {
4884                    LockableAmetysObject lockableContent = (LockableAmetysObject) refContent;
4885                    if (lockableContent.isLocked() && !LockHelper.isLockOwner(lockableContent, user))
4886                    {
4887                        User lockOwner = _userManager.getUser(lockableContent.getLockOwner().getPopulationId(), lockableContent.getLockOwner().getLogin());
4888                        
4889                        params.put("content", new I18nizableText(_contentHelper.getTitle(refContent)));
4890                        params.put("lockOwner", new I18nizableText(lockOwner != null ? lockOwner.getFullName() + " (" + lockOwner.getIdentity().getLogin() + ")" : lockableContent.getLockOwner().getLogin()));
4891                        errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_MUTUALRELATION_REFERENCED_CONTENT_LOCKED", params));
4892                        allErrors.addError(currentMetadataPath, errors);
4893                        
4894                        return false;
4895                    }
4896                }
4897                
4898                // Action in unavailable
4899                params.put("content", new I18nizableText(_contentHelper.getTitle(refContent)));
4900                errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_MUTUALRELATION_UNAVAILABLE_ACTION", params));
4901                allErrors.addError(currentMetadataPath, errors);
4902                
4903                return false;
4904            }
4905            else
4906            {
4907                return true;
4908            }
4909        }
4910        else
4911        {
4912            Errors errors = new Errors();
4913            Map<String, I18nizableText> params = new HashMap<>();
4914            params.put("content", new I18nizableText(_contentHelper.getTitle(refContent)));
4915            errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_MUTUALRELATION_NOWORKFLOWAWARE_CONTENT", params));
4916            allErrors.addError(currentMetadataPath, errors);
4917            return false;
4918        }
4919        
4920    }
4921    
4922    /**
4923     * Remove a mutual relation.
4924     * @param currentContentId the ID of the content being modified.
4925     * @param metaDef the metadata definition.
4926     * @param metaPath the metadata path.
4927     * @param refContentId the ID of the content being referenced.
4928     * @param editActionId The current 'edit content' action ID.
4929     * @param allErrors the errors.
4930     * @throws WorkflowException if a fatal error occurs.
4931     */
4932    protected void _removeInvertRelation(String currentContentId, MetadataDefinition metaDef, String metaPath, String refContentId, int editActionId, AllErrors allErrors) throws WorkflowException
4933    {
4934        try
4935        {
4936            String refContentTypeId = metaDef.getContentType();
4937            String refMetadataPath = metaDef.getInvertRelationPath();
4938            
4939            Content refContent = _resolver.resolveById(refContentId);
4940            
4941            if (refContent instanceof ModifiableContent && refMetadataPath != null)
4942            {
4943                ModifiableContent refModifiableContent = (ModifiableContent) refContent;
4944                
4945                ContentType refContentType = _contentTypeExtensionPoint.getExtension(refContentTypeId);
4946                MetadataDefinition refMetadataDef = refContentType.getMetadataDefinitionByPath(refMetadataPath);
4947                
4948                int lastSlashIndex = refMetadataPath.lastIndexOf(ContentConstants.METADATA_PATH_SEPARATOR);
4949                String refMetaName = lastSlashIndex > -1 ? refMetadataPath.substring(lastSlashIndex + 1) : refMetadataPath;
4950                
4951                try
4952                {
4953                    boolean needSave = true;
4954                    ModifiableCompositeMetadata refContentMeta = _getRelationMetaHolder(refModifiableContent, refContentType, refMetadataPath, currentContentId, false);
4955                    
4956                    if (refContentMeta != null)
4957                    {
4958                        Set<String> refExternalAndLocalMetadata = _externalizableMetadataProviderEP.getExternalAndLocalMetadata(refModifiableContent);
4959                        boolean externalizable = refExternalAndLocalMetadata.contains(refMetadataPath);
4960                        
4961                        if (refMetadataDef.isMultiple())
4962                        {
4963                            String[] values = refContentMeta.getStringArray(refMetaName, new String[0]);
4964                            int index = ArrayUtils.indexOf(values, currentContentId);
4965                            if (index > -1)
4966                            {
4967                                String[] newValues = ArrayUtils.remove(values, index);
4968                                
4969                                if (externalizable)
4970                                {
4971                                    ExternalizableMetadataHelper.setLocalMetadata(refContentMeta, refMetaName, newValues, ExternalizableMetadataStatus.LOCAL);
4972                                }
4973                                else
4974                                {
4975                                    ExternalizableMetadataHelper.setMetadata(refContentMeta, refMetaName, newValues);
4976                                }
4977                                needSave = true;
4978                            }
4979                        }
4980                        else
4981                        {
4982                            if (ExternalizableMetadataHelper.removeLocalMetadataIfExists(refContentMeta, refMetaName))
4983                            {
4984                                needSave = true;
4985                            }
4986                        }
4987                        
4988                        if (needSave)
4989                        {
4990                            refModifiableContent.saveChanges();
4991                            
4992                            // Trigger edit workflow action on the referenced content if needed.
4993                            _triggerEditWorkflowAction(refModifiableContent, editActionId);
4994                        }
4995                    }
4996                }
4997                catch (RepositoryException e)
4998                {
4999                    Errors errors = new Errors();
5000                    Map<String, I18nizableText> params = new HashMap<>();
5001                    params.put("content", new I18nizableText(_contentHelper.getTitle(refContent)));
5002                    params.put("error", new I18nizableText(e.getLocalizedMessage()));
5003                    errors.addError(new I18nizableText("plugin.cms", "CONTENT_EDITION_VALIDATION_ERRORS_MUTUALRELATION_REMOVE", params));
5004                    allErrors.addError(metaPath, errors);
5005                }
5006            }
5007        }
5008        catch (UnknownAmetysObjectException e)
5009        {
5010            // Ignore.
5011        }
5012    }
5013    
5014    /**
5015     * Remove a mutual relation.
5016     * @param currentContentId the ID of the content being modified.
5017     * @param metaDef the metadata definition.
5018     * @param metaPath the metadata path.
5019     * @param refContentIds the ID of the contents being referenced.
5020     * @param editActionId The current 'edit content' action ID.
5021     * @param allErrors the errors.
5022     * @throws WorkflowException if a fatal error occurs.
5023     */
5024    protected void _removeInvertRelations(String currentContentId, MetadataDefinition metaDef, String metaPath, String[] refContentIds, int editActionId, AllErrors allErrors) throws WorkflowException
5025    {
5026        for (String refContentId : refContentIds)
5027        {
5028            _removeInvertRelation(currentContentId, metaDef, metaPath, refContentId, editActionId, allErrors);
5029        }
5030    }
5031    
5032    /**
5033     * Get the metadata holder of the mutual relation.
5034     * @param refContent the content being referenced.
5035     * @param refContentType the content type of the content being referenced.
5036     * @param metadataPath the metadata path on the content being referenced, separated by '/'
5037     * @param searchContentId the ID of the content being modified.
5038     * @param create true to create non-existing composites and repeater entries.
5039     * @return The metadata holder of the relation, on the content being referenced.<br>
5040     *         Can be null if create is false and the metadata doesn't exist yet.
5041     * @throws RepositoryException if an error occurs.
5042     */
5043    protected ModifiableCompositeMetadata _getRelationMetaHolder(ModifiableContent refContent, ContentType refContentType, String metadataPath, String searchContentId, boolean create) throws RepositoryException
5044    {
5045        String[] pathSegments = StringUtils.split(metadataPath, ContentConstants.METADATA_PATH_SEPARATOR);
5046        
5047        if (pathSegments.length == 0)
5048        {
5049            return null;
5050        }
5051        
5052        ModifiableCompositeMetadata metadataHolder = refContent.getMetadataHolder();
5053        
5054        try
5055        {
5056            MetadataDefinition metadataDef = null;
5057            
5058            for (int i = 0; i < pathSegments.length - 1 && metadataHolder != null; i++)
5059            {
5060                if (i == 0)
5061                {
5062                    metadataDef = refContentType.getMetadataDefinition(pathSegments[0]);
5063                }
5064                else if (metadataDef != null)
5065                {
5066                    metadataDef = metadataDef.getMetadataDefinition(pathSegments[i]);
5067                }
5068                
5069                if (metadataDef != null && metadataDef.getType() == MetadataType.COMPOSITE)
5070                {
5071                    if (!create && !ExternalizableMetadataHelper.hasMetadata(metadataHolder, pathSegments[i], ExternalizableMetadataStatus.LOCAL))
5072                    {
5073                        return null;
5074                    }
5075
5076                    metadataHolder = ExternalizableMetadataHelper.getCompositeMetadata(metadataHolder, pathSegments[i]);
5077                
5078                    if (metadataDef instanceof RepeaterDefinition)
5079                    {
5080                        String[] repeaterPathSegments = ArrayUtils.subarray(pathSegments, i + 1, pathSegments.length);
5081                        RepeaterDefinition repeaterDef = (RepeaterDefinition) metadataDef;
5082                        
5083                        // Search the repeater entry referencing the content being modified (by its ID).
5084                        metadataHolder = _getEntryHolder(refContent, metadataHolder, repeaterDef, repeaterPathSegments, searchContentId, create);
5085                        if (!create && metadataHolder == null)
5086                        {
5087                            return null;
5088                        }
5089                    }
5090                }
5091            }
5092            
5093            return metadataHolder;
5094        }
5095        catch (UnknownMetadataException e)
5096        {
5097            // Ignore, just return null.
5098            return null;
5099        }
5100    }
5101
5102    /**
5103     * On a repeater metadata, search the entry referencing the content being modified.
5104     * @param refContent the content being referenced.
5105     * @param repeaterMeta the repeater metadata.
5106     * @param repeaterDef the repeater definition.
5107     * @param pathElements the path of the mutual relation from the repeater, as a String array.
5108     * @param searchContentId the ID of the content being modified.
5109     * @param create true to create non-existing composites and repeater entries.
5110     * @return The metadata holder of the relation, on the content being referenced.<br>
5111     *         Can be null if create is false and the metadata doesn't exist yet.
5112     * @throws RepositoryException if an error occurs.
5113     */
5114    private ModifiableCompositeMetadata _getEntryHolder(ModifiableContent refContent, ModifiableCompositeMetadata repeaterMeta, RepeaterDefinition repeaterDef, String[] pathElements, String searchContentId, boolean create) throws RepositoryException
5115    {
5116        int count = 0;
5117        int max = 0;
5118        for (String subMetaName : repeaterMeta.getMetadataNames())
5119        {
5120            if (repeaterMeta.getType(subMetaName) == org.ametys.plugins.repository.metadata.CompositeMetadata.MetadataType.COMPOSITE)
5121            {
5122                try
5123                {
5124                    count++;
5125                    max = Math.max(max, NumberUtils.toInt(subMetaName));
5126                    ModifiableCompositeMetadata entryHolder = ExternalizableMetadataHelper.getCompositeMetadata(repeaterMeta, subMetaName);
5127                    ModifiableCompositeMetadata meta = entryHolder;
5128                    
5129                    for (int i = 0; i < pathElements.length - 1 && meta != null; i++)
5130                    {
5131                        meta = ExternalizableMetadataHelper.getCompositeMetadata(meta, pathElements[i]);
5132                    }
5133                    
5134                    String[] ids = meta.getStringArray(pathElements[pathElements.length - 1], new String[0]);
5135                    if (ArrayUtils.contains(ids, searchContentId))
5136                    {
5137                        return entryHolder;
5138                    }
5139                }
5140                catch (UnknownMetadataException e)
5141                {
5142                    // Ignore.
5143                }
5144            }
5145        }
5146        
5147        if (create)
5148        {
5149            int maxSize = repeaterDef.getMaxSize();
5150            if (maxSize > 0 && count >= repeaterDef.getMaxSize())
5151            {
5152                throw new RepositoryException("Unable to create repeater entry in content " + refContent + ", max size attained (" + repeaterDef.getMaxSize() + ").");
5153            }
5154            
5155            String newEntryName = Integer.toString(max + 1);
5156            return ExternalizableMetadataHelper.getCompositeMetadata(repeaterMeta, newEntryName);
5157        }
5158        
5159        return null;
5160    }
5161    
5162    /**
5163     * Get the composite metadata holding the metadata given by its path
5164     * @param parentMetadata The parent composite metadata
5165     * @param metadataPath The metadata path (with /)
5166     * @return The direct parent metadata
5167     */
5168    protected ModifiableCompositeMetadata _getMetadataHolder(ModifiableCompositeMetadata parentMetadata, String metadataPath)
5169    {
5170        int pos = metadataPath.indexOf("/");
5171        if (pos == -1)
5172        {
5173            return parentMetadata;
5174        }
5175        else
5176        {
5177            return _getMetadataHolder(parentMetadata.getCompositeMetadata(metadataPath.substring(0, pos), true), metadataPath.substring(pos + 1));
5178        }
5179    }
5180    
5181    /**
5182     * Trigger a 'edit content' workflow action (if the content is workflow-aware).
5183     * @param content The content.
5184     * @param actionId The current 'edit content' action ID.
5185     * @throws WorkflowException if an error occurs.
5186     */
5187    protected void _triggerEditWorkflowAction(Content content, int actionId) throws WorkflowException
5188    {
5189        if (content instanceof WorkflowAwareContent)
5190        {
5191            Map<String, Object> inputs = new HashMap<>();
5192            Map<String, Object> parameters = new HashMap<>();
5193            
5194            inputs.put(EditContentFunction.EDIT_MUTUAL_RELATIONSHIP, true);
5195            inputs.put(AbstractWorkflowComponent.CONTEXT_PARAMETERS_KEY, parameters);
5196            
5197            // Do action regarless of user's rights because user's rights was already checked during preparing process
5198            // This is necessary because the removal of a invert relation could removed the user rights in a referenced content, whereas user has authorized to edit the content before this removal
5199            inputs.put(CheckRightsCondition.FORCE, true);
5200            
5201            parameters.put(FORM_RAW_VALUES, Collections.EMPTY_MAP); // No values
5202            parameters.put(QUIT, true);
5203            
5204            _workflowHelper.doAction((WorkflowAwareContent) content, actionId, inputs);
5205        }
5206    }
5207    
5208    private ExternalizableMetadataStatus _getExternalizableStatus (String rawValue)
5209    {
5210        Map<String, Object> externalizableValue = _jsonUtils.convertJsonToMap(rawValue);
5211        return ExternalizableMetadataStatus.valueOf(((String) externalizableValue.get("status")).toUpperCase());
5212    }
5213    
5214    private void _setMetadata(ModifiableCompositeMetadata metadata, String metadataName, Form form, Object value, boolean externalizable)
5215    {
5216        if (externalizable)
5217        {
5218            ExternalizableMetadataStatus status = form.getExternalizableField(metadataName).getStatus();
5219            ExternalizableMetadataHelper.setLocalMetadata(metadata, metadataName, value, status);
5220        }
5221        else
5222        {
5223            ExternalizableMetadataHelper.setMetadata(metadata, metadataName, value);
5224        }
5225    }
5226}