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