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