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