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