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            form.setId(id);
583            form.setLabel(label);
584            form.setReceiptFieldId(receiptFieldId);
585            form.setReceiptFieldBody(receiptFieldBody);
586            form.setReceiptFieldSubject(receiptFieldSubject);
587            form.setReceiptFieldFromAddress(receiptFieldFromAddress);
588            form.setNotificationEmails(new HashSet<>(emails));
589            form.setRedirectTo(redirectTo);
590            form.setWorkflowName(workflowName);
591            
592            _extractFields(formNode, form);
593        }
594        
595        return form;
596    }
597    
598    /**
599     * Extracts a form from a JCR Node.
600     * @param formNode the form node.
601     * @param form the form object.
602     * @throws RepositoryException if a repository error occurs.
603     */
604    protected void _extractFields(Node formNode, Form form) throws RepositoryException
605    {
606        NodeIterator nodes = formNode.getNodes();
607        while (nodes.hasNext())
608        {
609            Node node = nodes.nextNode();
610            
611            Field field = _extractField(node);
612            
613            form.getFields().add(field);
614        }
615    }
616    
617    /**
618     * Extracts a field from a JCR Node.
619     * @param fieldNode the field node.
620     * @return the Field object.
621     * @throws RepositoryException if a repository error occurs.
622     */
623    protected Field _extractField(Node fieldNode) throws RepositoryException
624    {
625        Field field = null;
626        
627        String id = _getSingleProperty(fieldNode, FIELD_PROPERTY_ID, "");
628        String type = _getSingleProperty(fieldNode, FIELD_PROPERTY_TYPE, "");
629        
630        // TODO Try/catch in case of enum name not found.
631        FieldType fieldType = FieldType.valueOf(type);
632        
633        if (!StringUtils.isEmpty(id) && !StringUtils.isEmpty(type))
634        {
635            String name = _getSingleProperty(fieldNode, FIELD_PROPERTY_NAME, "");
636            String label = _getSingleProperty(fieldNode, FIELD_PROPERTY_LABEL, "");
637            Map<String, String> properties = _getFieldProperties(fieldNode);
638            
639            field = new Field(fieldType);
640            
641            field.setId(id);
642            field.setName(name);
643            field.setLabel(label);
644            field.setProperties(properties);
645        }
646        
647        return field;
648    }
649    
650    /**
651     * Persist the form in a repository node.
652     * @param contentNode the content node in which the form is to be stored.
653     * @param form the form object to persist.
654     * @return the newly created form node.
655     * @throws RepositoryException if a repository error occurs while filling the node.
656     */
657    protected Node _storeForm(Node contentNode, Form form) throws RepositoryException
658    {
659        String name = form.getId();
660        if (StringUtils.isBlank(name))
661        {
662            name = "form";
663        }
664        
665        String nodeName = FilterNameHelper.filterName(name);
666        String notExistingNodeName = _getNotExistingNodeName(contentNode, nodeName);
667        
668        Node formNode = contentNode.addNode(notExistingNodeName);
669        formNode.addMixin(JcrConstants.MIX_REFERENCEABLE);
670        
671        return formNode;
672    }
673    
674    /**
675     * Fill a form node.
676     * @param formNode the form node.
677     * @param form the form object.
678     * @throws RepositoryException if a repository error occurs while filling the node.
679     * @throws FormsException if a forms error occurs while filling the node.
680     */
681    protected void _fillFormNode(Node formNode, Form form) throws RepositoryException, FormsException
682    {
683        Set<String> emails = form.getNotificationEmails();
684        String[] emailArray = emails.toArray(new String[emails.size()]);
685        
686        formNode.setProperty(FORM_PROPERTY_ID, form.getId());
687        formNode.setProperty(FORM_PROPERTY_LABEL, form.getLabel());
688        formNode.setProperty(FORM_PROPERTY_RECEIPT_FIELD_ID, form.getReceiptFieldId());
689        formNode.setProperty(FORM_PROPERTY_RECEIPT_BODY, form.getReceiptFieldBody());
690        formNode.setProperty(FORM_PROPERTY_RECEIPT_SUBJECT, form.getReceiptFieldSubject());
691        formNode.setProperty(FORM_PROPERTY_RECEIPT_FROM_ADDRESS, form.getReceiptFieldFromAddress());
692        formNode.setProperty(FORM_PROPERTY_EMAILS, emailArray);
693        formNode.setProperty(FORM_PROPERTY_REDIRECT_TO, form.getRedirectTo());
694        formNode.setProperty(FORM_PROPERTY_WORKFLOW_NAME, form.getWorkflowName());
695    }
696    
697    /**
698     * Store a field node.
699     * @param formNode the form node.
700     * @param field the field.
701     * @return the newly created field node.
702     * @throws RepositoryException if a repository error occurs while filling the node.
703     * @throws FormsException if a forms error occurs while filling the node.
704     */
705    protected Node _storeField(Node formNode, Field field) throws RepositoryException, FormsException
706    {
707        String name = field.getId();
708        if (StringUtils.isBlank(name))
709        {
710            name = "field";
711        }
712        
713        String nodeName = FilterNameHelper.filterName(name);
714        String notExistingNodeName = _getNotExistingNodeName(formNode, nodeName);
715        
716        Node fieldNode = formNode.addNode(notExistingNodeName);
717        
718        fieldNode.addMixin(JcrConstants.MIX_REFERENCEABLE);
719        
720        return fieldNode;
721    }
722    
723    /**
724     * Fill a field node.
725     * @param fieldNode the field node.
726     * @param field the field object.
727     * @throws RepositoryException if a repository error occurs while filling the node.
728     * @throws FormsException if a forms error occurs while filling the node.
729     */
730    protected void _fillFieldNode(Node fieldNode, Field field) throws RepositoryException, FormsException
731    {
732        fieldNode.setProperty(FIELD_PROPERTY_ID, field.getId());
733        fieldNode.setProperty(FIELD_PROPERTY_TYPE, field.getType().toString());
734        fieldNode.setProperty(FIELD_PROPERTY_NAME, field.getName());
735        fieldNode.setProperty(FIELD_PROPERTY_LABEL, field.getLabel());
736        
737        Map<String, String> fieldProperties = field.getProperties();
738        for (String propertyName : fieldProperties.keySet())
739        {
740            String value = fieldProperties.get(propertyName);
741            if (value != null)
742            {
743                String name = FIELD_PROPERTY_PREFIX + propertyName;
744                fieldNode.setProperty(name, value);
745            }
746        }
747    }    
748    /**
749     * Update the field nodes of a form.
750     * @param form the new form object.
751     * @param formNode the node of the form to update.
752     * @throws RepositoryException if a repository error occurs while updating the fields.
753     * @throws FormsException if a forms error occurs while updating the fields.
754     */
755    protected void _updateFields(Form form, Node formNode) throws RepositoryException, FormsException
756    {
757        Map<String, Field> fieldMap = form.getFieldMap();
758        
759        NodeIterator fieldNodes = formNode.getNodes();
760        
761        while (fieldNodes.hasNext())
762        {
763            Node fieldNode = fieldNodes.nextNode();
764            if (fieldNode.hasProperty(FIELD_PROPERTY_ID))
765            {
766                String fieldId = fieldNode.getProperty(FIELD_PROPERTY_ID).getString();
767                
768                // The field still exist in the new form : update its properties.
769                if (fieldMap.containsKey(fieldId))
770                {
771                    Field field = fieldMap.get(fieldId);
772                    
773                    _fillFieldNode(fieldNode, field);
774                    
775                    fieldMap.remove(fieldId);
776                }
777                else
778                {
779                    // The field doesn't exist anymore : delete it.
780                    fieldNode.remove();
781                }
782            }
783        }
784        
785        // Now the map contains the field to add.
786        for (Map.Entry<String, Field> entry : fieldMap.entrySet())
787        {
788            Field newField = entry.getValue();
789            Node fieldNode = _storeField(formNode, newField);
790            _fillFieldNode(fieldNode, newField);
791        }
792    }
793
794    /**
795     * Get a name for a node which doesn't already exist in this node.
796     * @param container the container node.
797     * @param baseName the base wanted node name.
798     * @return the name, free to be taken.
799     * @throws RepositoryException if a repository error occurs.
800     */
801    protected String _getNotExistingNodeName(Node container, String baseName) throws RepositoryException
802    {
803        String name = baseName;
804        
805        int index = 2;
806        while (container.hasNode(name))
807        {
808            name = baseName + index;
809            index++;
810        }
811        
812        return name;
813    }
814    
815    /**
816     * Get a single property value.
817     * @param node the JCR node.
818     * @param propertyName the name of the property to get.
819     * @param defaultValue the default value if the property does not exist.
820     * @return the single property value.
821     * @throws RepositoryException if a repository error occurs.
822     */
823    protected String _getSingleProperty(Node node, String propertyName, String defaultValue) throws RepositoryException
824    {
825        String value = defaultValue;
826        
827        if (node.hasProperty(propertyName))
828        {
829            value = node.getProperty(propertyName).getString();
830        }
831        
832        return value;
833    }
834    
835    /**
836     * Get the values of a string array property.
837     * @param node the node.
838     * @param propertyName the name of the property to get.
839     * @return the values.
840     * @throws RepositoryException if a repository error occurs.
841     */
842    protected Collection<String> _getMultipleProperty(Node node, String propertyName) throws RepositoryException
843    {
844        List<String> values = new ArrayList<>();
845        
846        if (node.hasProperty(propertyName))
847        {
848            Value[] propertyValues = node.getProperty(propertyName).getValues();
849            for (Value value : propertyValues)
850            {
851                values.add(value.getString());
852            }
853        }
854        
855        return values;
856    }
857    
858    /**
859     * Get additional configuration from properties.
860     * @param node the JCR node.
861     * @return the additional configuration as a Map.
862     * @throws RepositoryException if a repository error occurs.
863     */
864    protected Map<String, String> _getFieldProperties(Node node) throws RepositoryException
865    {
866        Map<String, String> values = new HashMap<>();
867        
868        PropertyIterator propertyIt = node.getProperties(FIELD_PROPERTY_PREFIX + "*");
869        while (propertyIt.hasNext())
870        {
871            Property property = propertyIt.nextProperty();
872            String propName = property.getName();
873            String name = propName.substring(FIELD_PROPERTY_PREFIX.length(), propName.length());
874            String value = property.getString();
875            
876            values.put(name, value);
877        }
878        
879        return values;
880    }
881    
882    /**
883     * Remove a form
884     * @param form The form to remove
885     * @param content The content holding the form
886     * @throws FormsException of an exception occurs when manipulating the forms' repository nodes
887     */
888    public void remove(Form form, Content content) throws FormsException
889    {
890        Session session = null;
891        try
892        {
893            session = _repository.login();
894            
895            String id = form.getId();
896            
897            String xpathQuery = "//element(*, ametys:content)[@jcr:uuid = '" + ((JCRAmetysObject) content).getNode().getIdentifier() + "']/" + FORMS_NODE + "/*[@" + FORM_PROPERTY_ID + " = '" + id + "']";
898            
899            QueryManager queryManager = session.getWorkspace().getQueryManager();
900            @SuppressWarnings("deprecation")
901            Query query = queryManager.createQuery(xpathQuery, Query.XPATH);
902            NodeIterator nodeIterator = query.execute().getNodes();
903            
904            if (nodeIterator.hasNext())
905            {
906                Node formNode = nodeIterator.nextNode();
907                formNode.remove();
908                
909                if (session.hasPendingChanges())
910                {
911                    session.save();
912                }
913            }
914        }
915        catch (RepositoryException e)
916        {
917            throw new FormsException("Repository exception while storing the form properties.", e);
918        }
919        finally
920        {
921            if (session != null)
922            {
923                session.logout();
924            }
925        }
926    }
927    
928    /**
929     * Get or create the forms node in a content node.
930     * @param baseNode the content base node.
931     * @return the forms node.
932     * @throws RepositoryException if an error occurs.
933     */
934    protected Node _createOrGetFormsNode(Node baseNode) throws RepositoryException
935    {
936        Node node = null;
937        if (baseNode.hasNode(FORMS_NODE))
938        {
939            node = baseNode.getNode(FORMS_NODE);
940        }
941        else
942        {
943            node = baseNode.addNode(FORMS_NODE, "nt:unstructured");
944        }
945        return node;
946    }
947}