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