001/*
002 *  Copyright 2010 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.plugins.forms.jcr;
017
018import java.util.ArrayList;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.HashSet;
022import java.util.List;
023import java.util.Map;
024import java.util.Set;
025import java.util.regex.Matcher;
026import java.util.regex.Pattern;
027
028import javax.jcr.Node;
029import javax.jcr.NodeIterator;
030import javax.jcr.Property;
031import javax.jcr.PropertyIterator;
032import javax.jcr.Repository;
033import javax.jcr.RepositoryException;
034import javax.jcr.Session;
035import javax.jcr.Value;
036import javax.jcr.query.Query;
037import javax.jcr.query.QueryManager;
038
039import org.apache.avalon.framework.component.Component;
040import org.apache.avalon.framework.logger.AbstractLogEnabled;
041import org.apache.avalon.framework.service.ServiceException;
042import org.apache.avalon.framework.service.ServiceManager;
043import org.apache.avalon.framework.service.Serviceable;
044import org.apache.commons.lang.StringUtils;
045import org.apache.jackrabbit.JcrConstants;
046
047import org.ametys.cms.FilterNameHelper;
048import org.ametys.cms.repository.Content;
049import org.ametys.cms.repository.DefaultContent;
050import org.ametys.plugins.forms.Field;
051import org.ametys.plugins.forms.Field.FieldType;
052import org.ametys.plugins.forms.Form;
053import org.ametys.plugins.forms.FormsException;
054import org.ametys.plugins.repository.AmetysObjectResolver;
055import org.ametys.plugins.repository.RepositoryConstants;
056import org.ametys.plugins.repository.jcr.JCRAmetysObject;
057import org.ametys.plugins.repository.provider.AbstractRepository;
058import org.ametys.runtime.plugin.component.PluginAware;
059import org.ametys.web.repository.site.SiteManager;
060
061/**
062 * Form properties manager : stores and retrieves form properties.
063 */
064public class FormPropertiesManager extends AbstractLogEnabled implements Serviceable, Component, PluginAware
065{
066    /** Pattern for options value */
067    public static final Pattern OPTION_VALUE_PATTERN = Pattern.compile("^option-([0-9]+)-value$");
068    
069    /** The avalon component ROLE. */
070    public static final String ROLE = FormPropertiesManager.class.getName();
071    
072    /** JCR relative path to root node. */
073    public static final String ROOT_REPO = AmetysObjectResolver.ROOT_REPO;
074    
075    /** Plugins root node name. */
076    public static final String PLUGINS_NODE = "ametys-internal:plugins";
077    
078    /** Forms node name. */
079    public static final String FORMS_NODE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":forms";
080    
081    /** Language property */
082    public static final String LANGUAGE_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":language";
083
084    /** Site property */
085    public static final String SITE_PROPERTY = RepositoryConstants.NAMESPACE_PREFIX + ":site";
086    
087    /** "ID" property name. */
088    public static final String FORM_PROPERTY_ID = RepositoryConstants.NAMESPACE_PREFIX + ":id";
089    
090    /** "Label" property name. */
091    public static final String FORM_PROPERTY_LABEL = RepositoryConstants.NAMESPACE_PREFIX + ":label";
092    
093    /** "Receipt field ID" property name. */
094    public static final String FORM_PROPERTY_RECEIPT_FIELD_ID = RepositoryConstants.NAMESPACE_PREFIX + ":receipt-field-id";
095    
096    /** "Receipt field ID" property name. */
097    public static final String FORM_PROPERTY_RECEIPT_FROM_ADDRESS = RepositoryConstants.NAMESPACE_PREFIX + ":receipt-from-address";
098    
099    /** "Receipt field ID" property name. */
100    public static final String FORM_PROPERTY_RECEIPT_SUBJECT = RepositoryConstants.NAMESPACE_PREFIX + ":receipt-subject";
101    
102    /** "Receipt field ID" property name. */
103    public static final String FORM_PROPERTY_RECEIPT_BODY = RepositoryConstants.NAMESPACE_PREFIX + ":receipt-body";
104    
105    /** The uuid of the page where to redirect to */
106    public static final String FORM_PROPERTY_REDIRECT_TO = RepositoryConstants.NAMESPACE_PREFIX + ":redirect-to";
107    
108    /** "Emails" property name. */
109    public static final String FORM_PROPERTY_EMAILS = RepositoryConstants.NAMESPACE_PREFIX + ":notification-emails";
110    
111    /** "Workflow name" property name. */
112    public static final String FORM_PROPERTY_WORKFLOW_NAME = RepositoryConstants.NAMESPACE_PREFIX + ":workflow-name";
113    
114    /** "ID" field property name. */
115    public static final String FIELD_PROPERTY_ID = RepositoryConstants.NAMESPACE_PREFIX + ":id";
116    
117    /** "Type" field property name. */
118    public static final String FIELD_PROPERTY_TYPE = RepositoryConstants.NAMESPACE_PREFIX + ":type";
119    
120    /** "Name" field property name. */
121    public static final String FIELD_PROPERTY_NAME = RepositoryConstants.NAMESPACE_PREFIX + ":name";
122    
123    /** "Label" field property name. */
124    public static final String FIELD_PROPERTY_LABEL = RepositoryConstants.NAMESPACE_PREFIX + ":label";
125    
126    /** Field properties prefix. */
127    public static final String FIELD_PROPERTY_PREFIX = RepositoryConstants.NAMESPACE_PREFIX + ":property-";
128    
129    /** The JCR repository. */
130    protected Repository _repository;
131    
132    /** The Site manager. */
133    protected SiteManager _siteManager;
134    
135    /** The resolver for ametys objects */
136    protected AmetysObjectResolver _resolver;
137    
138    /** The plugin name. */
139    protected String _pluginName;
140
141    @Override
142    public void service(ServiceManager serviceManager) throws ServiceException
143    {
144        _repository = (Repository) serviceManager.lookup(AbstractRepository.ROLE);
145        _siteManager = (SiteManager) serviceManager.lookup(SiteManager.ROLE);
146        _resolver = (AmetysObjectResolver) serviceManager.lookup(AmetysObjectResolver.ROLE);
147    }
148    
149    @Override
150    public void setPluginInfo(String pluginName, String featureName, String id)
151    {
152        _pluginName = pluginName;
153    }
154    
155    /**
156     * Get a form from the repository.
157     * @param id the form ID.
158     * @return the Form or null if no form with this ID exists.
159     * @throws FormsException if an error occurs.
160     */
161    public Form getForm(String id) throws FormsException
162    {
163        return getForm(null, id);
164    }
165    
166    /**
167     * Get a form from the repository.
168     * @param siteName the site name.
169     * @param id the form ID.
170     * @return the Form or null if no form with this ID exists.
171     * @throws FormsException if an error occurs.
172     */
173    public Form getForm(String siteName, String id) throws FormsException
174    {
175        Session session = null;
176        try
177        {
178            session = _repository.login();
179            
180            Form form = null;
181            
182            // Build the query
183            String xpathQuery = "//element(*, ametys:content)";
184            if (siteName != null)
185            {
186                xpathQuery += "[@" + SITE_PROPERTY + "='" + siteName + "']";
187            }
188            xpathQuery += "/" + FORMS_NODE + "/*[@" + FORM_PROPERTY_ID + " = '" + id + "']";
189            
190            QueryManager queryManager = session.getWorkspace().getQueryManager();
191            @SuppressWarnings("deprecation")
192            Query query = queryManager.createQuery(xpathQuery, Query.XPATH);
193            NodeIterator nodeIterator = query.execute().getNodes();
194            
195            if (nodeIterator.hasNext())
196            {
197                Node node = nodeIterator.nextNode();
198                
199                form = _extractForm(node);
200            }
201            
202            return form;
203        }
204        catch (RepositoryException e)
205        {
206            throw new FormsException("Error executing the query to find the form of id " + id, e);
207        }
208        finally
209        {
210            if (session != null)
211            {
212                session.logout();
213            }
214        }
215    }
216    
217    /**
218     * Get the most recent frozen node that contain the form of the given id 
219     * @param formId the id of the form
220     * @return the list of frozen content nodes containing the form
221     * @throws FormsException if an error occurs while retrieving the forms' frozen content nodes
222     */
223    public Node getMostRecentFormFrozenContent(String formId) throws FormsException
224    {
225        Session session = null;
226        try
227        {
228            session = _repository.login();
229            
230            String xpathQuery = "//element(" + formId + ", nt:frozenNode)/../.. order by @" + RepositoryConstants.NAMESPACE_PREFIX + ":" + DefaultContent.METADATA_MODIFIED + " descending";
231            
232            QueryManager queryManager = session.getWorkspace().getQueryManager();
233            @SuppressWarnings("deprecation")
234            Query query = queryManager.createQuery(xpathQuery, Query.XPATH);
235            NodeIterator nodeIterator = query.execute().getNodes();
236            
237            if (nodeIterator.hasNext())
238            {
239                return (Node) nodeIterator.next();
240            }
241            
242            return null;
243        }
244        catch (RepositoryException e)
245        {
246            getLogger().error("Error executing the query to find the frozen content nodes for the form '" + formId + "'.", e);
247            return null;
248        }
249        finally
250        {
251            if (session != null)
252            {
253                session.logout();
254            }
255        }
256    }
257    
258    /**
259     * Get the forms in a specified content.
260     * @param content the content.
261     * @return the forms as a list.
262     * @throws FormsException if an error occurs.
263     */
264    public List<Form> getForms(Content content) throws FormsException
265    {
266        Session session = null;
267        try
268        {
269            List<Form> forms = new ArrayList<>();
270            
271            session = _repository.login();
272            
273            // FIXME API getNode should be VersionableAmetysObject
274            if (content instanceof JCRAmetysObject)
275            {
276                Node contentNode = ((JCRAmetysObject) content).getNode();
277                
278                if (contentNode.hasNode(FORMS_NODE))
279                {
280                    Node formsNode = contentNode.getNode(FORMS_NODE);
281                    
282                    NodeIterator nodes = formsNode.getNodes();
283                    
284                    while (nodes.hasNext())
285                    {
286                        Node node = nodes.nextNode();
287                        
288                        Form form = _extractForm(node);
289                        
290                        if (form != null)
291                        {
292                            forms.add(form);
293                        }
294                    }
295                }
296            }
297            return forms;
298        }
299        catch (RepositoryException e)
300        {
301            getLogger().error("Error getting forms for a content.", e);
302            throw new FormsException("Error getting forms for a content.", e);
303        }
304        finally
305        {
306            if (session != null)
307            {
308                session.logout();
309            }
310        }
311    }
312    
313    /**
314     * Get all the contents containing at least one form of the given site with the given language
315     * @param siteName the site name.
316     * @param language the language
317     * @return the forms' list or null if none was found
318     * @throws FormsException if an error occurs.
319     */
320    public List<Node> getFormContentNodes(String siteName, String language) throws FormsException
321    {
322        List<Node> contentNodes = new ArrayList<> ();
323        Session session = null;
324        try
325        {
326            session = _repository.login();
327            
328            String xpathQuery = "//element(*, ametys:content)[@" + SITE_PROPERTY + " = '" + siteName + "' and @" + LANGUAGE_PROPERTY + " = '" + language + "']/" + FORMS_NODE;
329            
330            QueryManager queryManager = session.getWorkspace().getQueryManager();
331            @SuppressWarnings("deprecation")
332            Query query = queryManager.createQuery(xpathQuery, Query.XPATH);
333            NodeIterator nodeIterator = query.execute().getNodes();
334            
335            while (nodeIterator.hasNext())
336            {
337                Node formNode = nodeIterator.nextNode();
338                contentNodes.add(formNode.getParent());
339            }
340            
341            return contentNodes;
342        }
343        catch (RepositoryException e)
344        {
345            throw new FormsException("Error executing the query to find the forms of the site '" + siteName + "' and of language '" + language + "'.", e);
346        }
347        finally
348        {
349            if (session != null)
350            {
351                session.logout();
352            }
353        }
354    }   
355    
356    /**
357     * Get the content containing the form with the given id
358     * @param formId the id of the form
359     * @return the {@link Content} containing the form or <code>null</code> if not found
360     * @throws FormsException if something goes wrong when either querying the form JCR node or finding its parent {@link Content} 
361     */
362    public Content getFormContent(String formId) throws FormsException
363    {
364        Session session = null;
365        try
366        {
367            session = _repository.login();
368            
369            // Build the query
370            String xpathQuery = "//element(*, ametys:content)/" + FORMS_NODE + "/*[@" + FORM_PROPERTY_ID + " = '" + formId + "']";
371            
372            // Execute
373            QueryManager queryManager = session.getWorkspace().getQueryManager();
374            @SuppressWarnings("deprecation")
375            Query query = queryManager.createQuery(xpathQuery, Query.XPATH);
376            NodeIterator nodeIterator = query.execute().getNodes();
377            
378            if (nodeIterator.hasNext())
379            {
380                Node node = nodeIterator.nextNode().getParent().getParent();
381                Content content = (Content) _resolver.resolve(node, false);
382                return content;
383            }
384            
385            return null;
386        }
387        catch (RepositoryException e)
388        {
389            throw new FormsException("Error executing the query to find the content containing the form of id " + formId, e);
390        }
391        finally
392        {
393            if (session != null)
394            {
395                session.logout();
396            }
397        }
398        
399    }
400    
401    
402    /**
403     * Extract all the form objects from a node
404     * @param node the node
405     * @return the forms list of this node
406     * @throws FormsException if an error occurs
407     * @throws RepositoryException if an error occurs when getting the properties of a node
408     */
409    public List<Form> getForms(Node node) throws FormsException, RepositoryException
410    {
411        List<Form> forms = new ArrayList<> ();
412        try
413        {
414            if (node.hasNode(FORMS_NODE))
415            {
416                Node formsNode = node.getNode(FORMS_NODE);
417                if (formsNode != null)
418                {
419                    NodeIterator formsNodeIterator = formsNode.getNodes();
420                    while (formsNodeIterator.hasNext())
421                    {
422                        Node formNode = formsNodeIterator.nextNode();
423                        Form form = _extractForm(formNode);
424                        if (form != null)
425                        {
426                            forms.add(form);
427                        }
428                    }
429                }
430            }
431            
432            return forms;
433        }
434        catch (RepositoryException e)
435        {
436            throw new FormsException("Error executing the query to find the forms of the node '" + node.getName() + "' (" + node.getIdentifier() + ").", e);
437        }
438    }
439    
440    /**
441     * Store the properties of a form in the repository.
442     * @param siteName the site name.
443     * @param form the form object.
444     * @param content the form content.
445     * @throws FormsException if an error occurs storing the form.
446     */
447    public void createForm(String siteName, Form form, Content content) throws FormsException
448    {
449        try
450        {
451            // FIXME API getNode should be VersionableAmetysObject
452            if (content instanceof JCRAmetysObject)
453            {
454                Node contentNode = ((JCRAmetysObject) content).getNode();
455                
456                Node formsNode = _createOrGetFormsNode(contentNode);
457                
458                Node formNode = _storeForm(formsNode, form);
459                
460                _fillFormNode(formNode, form);
461                
462                for (Field field : form.getFields())
463                {
464                    Node fieldNode = _storeField(formNode, field);
465                    _fillFieldNode(fieldNode, field);
466                }
467                
468                contentNode.getSession().save();
469            }
470        }
471        catch (RepositoryException e)
472        {
473            throw new FormsException("Repository exception while storing the form properties.", e);
474        }
475    }
476    
477    /**
478     * Update the properties of a form in the repository.
479     * @param siteName the site name.
480     * @param form the form object.
481     * @param content the form content.
482     * @throws FormsException if an error occurs storing the form.
483     */
484    public void updateForm(String siteName, Form form, Content content) throws FormsException
485    {
486        Session session = null;
487        try
488        {
489            session = _repository.login();
490            
491            String id = form.getId();
492            
493            // FIXME API getNode should be VersionableAmetysObject
494            if (content instanceof JCRAmetysObject)
495            {
496                String xpathQuery = "//element(*, ametys:content)/" + FORMS_NODE + "/*[@" + FORM_PROPERTY_ID + " = '" + id + "']";
497                
498                QueryManager queryManager = session.getWorkspace().getQueryManager();
499                @SuppressWarnings("deprecation")
500                Query query = queryManager.createQuery(xpathQuery, Query.XPATH);
501                NodeIterator nodeIterator = query.execute().getNodes();
502                
503                if (nodeIterator.hasNext())
504                {
505                    Node formNode = nodeIterator.nextNode();
506                    
507                    _fillFormNode(formNode, form);
508                    
509                    _updateFields(form, formNode);
510                    
511                    if (session.hasPendingChanges())
512                    {
513                        session.save();
514                    }
515                }
516            }
517        }
518        catch (RepositoryException e)
519        {
520            throw new FormsException("Repository exception while storing the form properties.", e);
521        }
522        finally
523        {
524            if (session != null)
525            {
526                session.logout();
527            }
528        }
529    }
530    
531    /**
532     * Get the value for display
533     * @param field The field
534     * @param value The value
535     * @return The value to display
536     */
537    public String getDisplayValue (Field field, String value)
538    {
539        Map<String, String> properties = field.getProperties();
540        for (String key : properties.keySet())
541        {
542            Matcher matcher = OPTION_VALUE_PATTERN.matcher(key);
543            if (matcher.matches())
544            {
545                if (value.equals(properties.get(key)))
546                {
547                    String index = matcher.group(1);
548                    if (properties.containsKey("option-" + index + "-label"))
549                    {
550                        return properties.get("option-" + index + "-label");
551                    }
552                }
553            }
554        }
555        return value;
556    }
557    
558    /**
559     * Extracts a form from a JCR Node.
560     * @param formNode the form node.
561     * @return the Form object.
562     * @throws RepositoryException if a repository error occurs.
563     */
564    protected Form _extractForm(Node formNode) throws RepositoryException
565    {
566        Form form = null;
567        
568        String id = _getSingleProperty(formNode, FORM_PROPERTY_ID, "");
569        if (!StringUtils.isEmpty(id))
570        {
571            String label = _getSingleProperty(formNode, FORM_PROPERTY_LABEL, "");
572            String receiptFieldId = _getSingleProperty(formNode, FORM_PROPERTY_RECEIPT_FIELD_ID, "");
573            String receiptFieldBody = _getSingleProperty(formNode, FORM_PROPERTY_RECEIPT_BODY, "");
574            String receiptFieldSubject = _getSingleProperty(formNode, FORM_PROPERTY_RECEIPT_SUBJECT, "");
575            String receiptFieldFromAddress = _getSingleProperty(formNode, FORM_PROPERTY_RECEIPT_FROM_ADDRESS, "");
576            String redirectTo = _getSingleProperty(formNode, FORM_PROPERTY_REDIRECT_TO, "");
577            Collection<String> emails = _getMultipleProperty(formNode, FORM_PROPERTY_EMAILS);
578            String workflowName = _getSingleProperty(formNode, FORM_PROPERTY_WORKFLOW_NAME, "");
579            
580            form = new Form();
581            
582            Content content = _resolver.resolve(formNode.getParent().getParent(), false);
583            form.setContentId(content.getId());
584            
585            form.setId(id);
586            form.setLabel(label);
587            form.setReceiptFieldId(receiptFieldId);
588            form.setReceiptFieldBody(receiptFieldBody);
589            form.setReceiptFieldSubject(receiptFieldSubject);
590            form.setReceiptFieldFromAddress(receiptFieldFromAddress);
591            form.setNotificationEmails(new HashSet<>(emails));
592            form.setRedirectTo(redirectTo);
593            form.setWorkflowName(workflowName);
594            
595            _extractFields(formNode, form);
596        }
597        
598        return form;
599    }
600    
601    /**
602     * Extracts a form from a JCR Node.
603     * @param formNode the form node.
604     * @param form the form object.
605     * @throws RepositoryException if a repository error occurs.
606     */
607    protected void _extractFields(Node formNode, Form form) throws RepositoryException
608    {
609        NodeIterator nodes = formNode.getNodes();
610        while (nodes.hasNext())
611        {
612            Node node = nodes.nextNode();
613            
614            Field field = _extractField(node);
615            
616            form.getFields().add(field);
617        }
618    }
619    
620    /**
621     * Extracts a field from a JCR Node.
622     * @param fieldNode the field node.
623     * @return the Field object.
624     * @throws RepositoryException if a repository error occurs.
625     */
626    protected Field _extractField(Node fieldNode) throws RepositoryException
627    {
628        Field field = null;
629        
630        String id = _getSingleProperty(fieldNode, FIELD_PROPERTY_ID, "");
631        String type = _getSingleProperty(fieldNode, FIELD_PROPERTY_TYPE, "");
632        
633        // TODO Try/catch in case of enum name not found.
634        FieldType fieldType = FieldType.valueOf(type);
635        
636        if (!StringUtils.isEmpty(id) && !StringUtils.isEmpty(type))
637        {
638            String name = _getSingleProperty(fieldNode, FIELD_PROPERTY_NAME, "");
639            String label = _getSingleProperty(fieldNode, FIELD_PROPERTY_LABEL, "");
640            Map<String, String> properties = _getFieldProperties(fieldNode);
641            
642            field = new Field(fieldType);
643            
644            field.setId(id);
645            field.setName(name);
646            field.setLabel(label);
647            field.setProperties(properties);
648        }
649        
650        return field;
651    }
652    
653    /**
654     * Persist the form in a repository node.
655     * @param contentNode the content node in which the form is to be stored.
656     * @param form the form object to persist.
657     * @return the newly created form node.
658     * @throws RepositoryException if a repository error occurs while filling the node.
659     */
660    protected Node _storeForm(Node contentNode, Form form) throws RepositoryException
661    {
662        String name = form.getId();
663        if (StringUtils.isBlank(name))
664        {
665            name = "form";
666        }
667        
668        String nodeName = FilterNameHelper.filterName(name);
669        String notExistingNodeName = _getNotExistingNodeName(contentNode, nodeName);
670        
671        Node formNode = contentNode.addNode(notExistingNodeName);
672        formNode.addMixin(JcrConstants.MIX_REFERENCEABLE);
673        
674        return formNode;
675    }
676    
677    /**
678     * Fill a form node.
679     * @param formNode the form node.
680     * @param form the form object.
681     * @throws RepositoryException if a repository error occurs while filling the node.
682     * @throws FormsException if a forms error occurs while filling the node.
683     */
684    protected void _fillFormNode(Node formNode, Form form) throws RepositoryException, FormsException
685    {
686        Set<String> emails = form.getNotificationEmails();
687        String[] emailArray = emails.toArray(new String[emails.size()]);
688        
689        formNode.setProperty(FORM_PROPERTY_ID, form.getId());
690        formNode.setProperty(FORM_PROPERTY_LABEL, form.getLabel());
691        formNode.setProperty(FORM_PROPERTY_RECEIPT_FIELD_ID, form.getReceiptFieldId());
692        formNode.setProperty(FORM_PROPERTY_RECEIPT_BODY, form.getReceiptFieldBody());
693        formNode.setProperty(FORM_PROPERTY_RECEIPT_SUBJECT, form.getReceiptFieldSubject());
694        formNode.setProperty(FORM_PROPERTY_RECEIPT_FROM_ADDRESS, form.getReceiptFieldFromAddress());
695        formNode.setProperty(FORM_PROPERTY_EMAILS, emailArray);
696        formNode.setProperty(FORM_PROPERTY_REDIRECT_TO, form.getRedirectTo());
697        formNode.setProperty(FORM_PROPERTY_WORKFLOW_NAME, form.getWorkflowName());
698    }
699    
700    /**
701     * Store a field node.
702     * @param formNode the form node.
703     * @param field the field.
704     * @return the newly created field node.
705     * @throws RepositoryException if a repository error occurs while filling the node.
706     * @throws FormsException if a forms error occurs while filling the node.
707     */
708    protected Node _storeField(Node formNode, Field field) throws RepositoryException, FormsException
709    {
710        String name = field.getId();
711        if (StringUtils.isBlank(name))
712        {
713            name = "field";
714        }
715        
716        String nodeName = FilterNameHelper.filterName(name);
717        String notExistingNodeName = _getNotExistingNodeName(formNode, nodeName);
718        
719        Node fieldNode = formNode.addNode(notExistingNodeName);
720        
721        fieldNode.addMixin(JcrConstants.MIX_REFERENCEABLE);
722        
723        return fieldNode;
724    }
725    
726    /**
727     * Fill a field node.
728     * @param fieldNode the field node.
729     * @param field the field object.
730     * @throws RepositoryException if a repository error occurs while filling the node.
731     * @throws FormsException if a forms error occurs while filling the node.
732     */
733    protected void _fillFieldNode(Node fieldNode, Field field) throws RepositoryException, FormsException
734    {
735        fieldNode.setProperty(FIELD_PROPERTY_ID, field.getId());
736        fieldNode.setProperty(FIELD_PROPERTY_TYPE, field.getType().toString());
737        fieldNode.setProperty(FIELD_PROPERTY_NAME, field.getName());
738        fieldNode.setProperty(FIELD_PROPERTY_LABEL, field.getLabel());
739        
740        Map<String, String> fieldProperties = field.getProperties();
741        for (String propertyName : fieldProperties.keySet())
742        {
743            String value = fieldProperties.get(propertyName);
744            if (value != null)
745            {
746                String name = FIELD_PROPERTY_PREFIX + propertyName;
747                fieldNode.setProperty(name, value);
748            }
749        }
750    }    
751    /**
752     * Update the field nodes of a form.
753     * @param form the new form object.
754     * @param formNode the node of the form to update.
755     * @throws RepositoryException if a repository error occurs while updating the fields.
756     * @throws FormsException if a forms error occurs while updating the fields.
757     */
758    protected void _updateFields(Form form, Node formNode) throws RepositoryException, FormsException
759    {
760        Map<String, Field> fieldMap = form.getFieldMap();
761        
762        NodeIterator fieldNodes = formNode.getNodes();
763        
764        while (fieldNodes.hasNext())
765        {
766            Node fieldNode = fieldNodes.nextNode();
767            if (fieldNode.hasProperty(FIELD_PROPERTY_ID))
768            {
769                String fieldId = fieldNode.getProperty(FIELD_PROPERTY_ID).getString();
770                
771                // The field still exist in the new form : update its properties.
772                if (fieldMap.containsKey(fieldId))
773                {
774                    Field field = fieldMap.get(fieldId);
775                    
776                    _fillFieldNode(fieldNode, field);
777                    
778                    fieldMap.remove(fieldId);
779                }
780                else
781                {
782                    // The field doesn't exist anymore : delete it.
783                    fieldNode.remove();
784                }
785            }
786        }
787        
788        // Now the map contains the field to add.
789        for (Map.Entry<String, Field> entry : fieldMap.entrySet())
790        {
791            Field newField = entry.getValue();
792            Node fieldNode = _storeField(formNode, newField);
793            _fillFieldNode(fieldNode, newField);
794        }
795    }
796
797    /**
798     * Get a name for a node which doesn't already exist in this node.
799     * @param container the container node.
800     * @param baseName the base wanted node name.
801     * @return the name, free to be taken.
802     * @throws RepositoryException if a repository error occurs.
803     */
804    protected String _getNotExistingNodeName(Node container, String baseName) throws RepositoryException
805    {
806        String name = baseName;
807        
808        int index = 2;
809        while (container.hasNode(name))
810        {
811            name = baseName + index;
812            index++;
813        }
814        
815        return name;
816    }
817    
818    /**
819     * Get a single property value.
820     * @param node the JCR node.
821     * @param propertyName the name of the property to get.
822     * @param defaultValue the default value if the property does not exist.
823     * @return the single property value.
824     * @throws RepositoryException if a repository error occurs.
825     */
826    protected String _getSingleProperty(Node node, String propertyName, String defaultValue) throws RepositoryException
827    {
828        String value = defaultValue;
829        
830        if (node.hasProperty(propertyName))
831        {
832            value = node.getProperty(propertyName).getString();
833        }
834        
835        return value;
836    }
837    
838    /**
839     * Get the values of a string array property.
840     * @param node the node.
841     * @param propertyName the name of the property to get.
842     * @return the values.
843     * @throws RepositoryException if a repository error occurs.
844     */
845    protected Collection<String> _getMultipleProperty(Node node, String propertyName) throws RepositoryException
846    {
847        List<String> values = new ArrayList<>();
848        
849        if (node.hasProperty(propertyName))
850        {
851            Value[] propertyValues = node.getProperty(propertyName).getValues();
852            for (Value value : propertyValues)
853            {
854                values.add(value.getString());
855            }
856        }
857        
858        return values;
859    }
860    
861    /**
862     * Get additional configuration from properties.
863     * @param node the JCR node.
864     * @return the additional configuration as a Map.
865     * @throws RepositoryException if a repository error occurs.
866     */
867    protected Map<String, String> _getFieldProperties(Node node) throws RepositoryException
868    {
869        Map<String, String> values = new HashMap<>();
870        
871        PropertyIterator propertyIt = node.getProperties(FIELD_PROPERTY_PREFIX + "*");
872        while (propertyIt.hasNext())
873        {
874            Property property = propertyIt.nextProperty();
875            String propName = property.getName();
876            String name = propName.substring(FIELD_PROPERTY_PREFIX.length(), propName.length());
877            String value = property.getString();
878            
879            values.put(name, value);
880        }
881        
882        return values;
883    }
884    
885    /**
886     * Remove a form
887     * @param form The form to remove
888     * @param content The content holding the form
889     * @throws FormsException of an exception occurs when manipulating the forms' repository nodes
890     */
891    public void remove(Form form, Content content) throws FormsException
892    {
893        Session session = null;
894        try
895        {
896            session = _repository.login();
897            
898            String id = form.getId();
899            
900            String xpathQuery = "//element(*, ametys:content)[@jcr:uuid = '" + ((JCRAmetysObject) content).getNode().getIdentifier() + "']/" + FORMS_NODE + "/*[@" + FORM_PROPERTY_ID + " = '" + id + "']";
901            
902            QueryManager queryManager = session.getWorkspace().getQueryManager();
903            @SuppressWarnings("deprecation")
904            Query query = queryManager.createQuery(xpathQuery, Query.XPATH);
905            NodeIterator nodeIterator = query.execute().getNodes();
906            
907            if (nodeIterator.hasNext())
908            {
909                Node formNode = nodeIterator.nextNode();
910                formNode.remove();
911                
912                if (session.hasPendingChanges())
913                {
914                    session.save();
915                }
916            }
917        }
918        catch (RepositoryException e)
919        {
920            throw new FormsException("Repository exception while storing the form properties.", e);
921        }
922        finally
923        {
924            if (session != null)
925            {
926                session.logout();
927            }
928        }
929    }
930    
931    /**
932     * Get or create the forms node in a content node.
933     * @param baseNode the content base node.
934     * @return the forms node.
935     * @throws RepositoryException if an error occurs.
936     */
937    protected Node _createOrGetFormsNode(Node baseNode) throws RepositoryException
938    {
939        Node node = null;
940        if (baseNode.hasNode(FORMS_NODE))
941        {
942            node = baseNode.getNode(FORMS_NODE);
943        }
944        else
945        {
946            node = baseNode.addNode(FORMS_NODE, "nt:unstructured");
947        }
948        return node;
949    }
950}