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