001/*
002 *  Copyright 2011 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.survey.repository;
017
018import java.util.LinkedHashMap;
019import java.util.LinkedList;
020import java.util.List;
021import java.util.Map;
022
023import javax.jcr.Node;
024import javax.jcr.NodeIterator;
025import javax.jcr.PathNotFoundException;
026import javax.jcr.RepositoryException;
027
028import org.apache.commons.lang.StringUtils;
029
030import org.ametys.plugins.repository.AmetysObject;
031import org.ametys.plugins.repository.AmetysRepositoryException;
032import org.ametys.plugins.repository.ModifiableTraversableAmetysObject;
033import org.ametys.plugins.repository.RepositoryConstants;
034import org.ametys.plugins.survey.repository.SurveyRule.RuleType;
035
036/**
037 * {@link AmetysObject} for storing survey
038 */
039public class SurveyQuestion extends AbstractSurveyElement<SurveyQuestionFactory>
040{
041    /** Prefix for options */
042    public static final String OPTION_NAME_PREFIX = "opt-";
043    
044    /** Constants for title metadata. */
045    private static final String __PROPERTY_LABEL = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":label";
046    /** Constants for title metadata. */
047    private static final String __PROPERTY_TITLE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":title";
048    /** Constants for type metadata. */
049    private static final String __PROPERTY_TYPE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":type";
050    /** Constants for regexp metadata. */
051    private static final String __PROPERTY_REGEXP = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":regexp";
052    /** Constants for mandatory metadata. */
053    private static final String __PROPERTY_MANDATORY = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":mandatory";
054    /** Constants for other option metadata. */
055    private static final String __PROPERTY_OTHER_OPTION = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":other-option";
056    /** Constants for options metadata. */
057    private static final String __NODE_NAME_OPTIONS = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":options";
058    /** Constants for columns metadata. */
059    private static final String __NODE_NAME_COLUMNS = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":columns";
060    /** Constants for rules metadata. */
061    private static final String __NODE_NAME_RULES = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":rules";
062    /** Constants for rule metadata. */
063    private static final String __PROPERTY_RULE_TYPE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":rule";
064    private static final String __PROPERTY_RULE_OPTION = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":option";
065    private static final String __PROPERTY_RULE_PAGE = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":page";
066    
067    
068    /** Type of a page. */
069    public enum QuestionType
070    {
071        /** Free text. */
072        FREE_TEXT,
073        /** Multiline free text. */
074        MULTILINE_FREE_TEXT,
075        /** Single choice. */
076        SINGLE_CHOICE,
077        /** Multiple choice. */
078        MULTIPLE_CHOICE,
079        /** Matrix of single choice. */
080        SINGLE_MATRIX,
081        /** Matrix of multiple choice. */
082        MULTIPLE_MATRIX
083    }
084    
085    /**
086     * Creates a {@link SurveyQuestion}.
087     * @param node the node backing this {@link AmetysObject}.
088     * @param parentPath the parent path in the Ametys hierarchy.
089     * @param factory the {@link SurveyFactory} which creates the AmetysObject.
090     */
091    public SurveyQuestion(Node node, String parentPath, SurveyQuestionFactory factory)
092    {
093        super(node, parentPath, factory);
094    }
095    
096    /**
097     * Retrieves the label.
098     * @return the label.
099     * @throws AmetysRepositoryException if an error occurs.
100     */
101    public String getLabel() throws AmetysRepositoryException
102    {
103        try
104        {
105            return getNode().getProperty(__PROPERTY_LABEL).getString();
106        }
107        catch (PathNotFoundException e)
108        {
109            return null;
110        }
111        catch (RepositoryException e)
112        {
113            throw new AmetysRepositoryException("Unable to get label property", e);
114        }
115    }
116    
117    /**
118     * Set the label.
119     * @param label the label.
120     * @throws AmetysRepositoryException if an error occurs.
121     */
122    public void setLabel(String label) throws AmetysRepositoryException
123    {
124        try
125        {
126            getNode().setProperty(__PROPERTY_LABEL, label);
127        }
128        catch (RepositoryException e)
129        {
130            throw new AmetysRepositoryException("Unable to set label property", e);
131        }
132    }
133
134    /**
135     * Retrieves the title.
136     * @return the title.
137     * @throws AmetysRepositoryException if an error occurs.
138     */
139    public String getTitle() throws AmetysRepositoryException
140    {
141        try
142        {
143            return getNode().getProperty(__PROPERTY_TITLE).getString();
144        }
145        catch (PathNotFoundException e)
146        {
147            return null;
148        }
149        catch (RepositoryException e)
150        {
151            throw new AmetysRepositoryException("Unable to get title property", e);
152        }
153    }
154    
155    /**
156     * Set the title.
157     * @param title the title.
158     * @throws AmetysRepositoryException if an error occurs.
159     */
160    public void setTitle(String title) throws AmetysRepositoryException
161    {
162        try
163        {
164            getNode().setProperty(__PROPERTY_TITLE, title);
165        }
166        catch (RepositoryException e)
167        {
168            throw new AmetysRepositoryException("Unable to set title property", e);
169        }
170    }
171    
172    /**
173     * Retrieves the type.
174     * @return the type.
175     * @throws AmetysRepositoryException if an error occurs.
176     */
177    public QuestionType getType() throws AmetysRepositoryException
178    {
179        try
180        {
181            return QuestionType.valueOf(getNode().getProperty(__PROPERTY_TYPE).getString());
182        }
183        catch (RepositoryException e)
184        {
185            throw new AmetysRepositoryException("Unable to get type property", e);
186        }
187    }
188    
189    /**
190     * Set the type.
191     * @param type the type.
192     * @throws AmetysRepositoryException if an error occurs.
193     */
194    public void setType(QuestionType type) throws AmetysRepositoryException
195    {
196        try
197        {
198            getNode().setProperty(__PROPERTY_TYPE, type.name());
199        }
200        catch (RepositoryException e)
201        {
202            throw new AmetysRepositoryException("Unable to set type property", e);
203        }
204    }
205    
206    /**
207     * Retrieves the regexp type.
208     * @return the regexp type.
209     * @throws AmetysRepositoryException if an error occurs.
210     */
211    public String getRegExpType() throws AmetysRepositoryException
212    {
213        try
214        {
215            return getNode().getProperty(__PROPERTY_REGEXP).getString();
216        }
217        catch (PathNotFoundException e)
218        {
219            return null;
220        }
221        catch (RepositoryException e)
222        {
223            throw new AmetysRepositoryException("Unable to get regexp property", e);
224        }
225    }
226    
227    /**
228     * Set the regexp type.
229     * @param regexp the regexp type.
230     * @throws AmetysRepositoryException if an error occurs.
231     */
232    public void setRegExpType(String regexp) throws AmetysRepositoryException
233    {
234        try
235        {
236            getNode().setProperty(__PROPERTY_REGEXP, regexp);
237        }
238        catch (RepositoryException e)
239        {
240            throw new AmetysRepositoryException("Unable to set regexp property", e);
241        }
242    }
243    
244    /**
245     * Get the validation pattern.
246     * @return the validation pattern.
247     * @throws AmetysRepositoryException if an error occurs.
248     */
249    public String getRegExpPattern () throws AmetysRepositoryException
250    {
251        String regexpType = getRegExpType();
252        if (regexpType == null)
253        {
254            return null;
255        }
256        
257        if ("int".equals(regexpType))
258        {
259            return "^-?[0-9]+$";
260        }
261        else if ("float".equals(regexpType))
262        {
263            return "^-?[0-9]+(\\.[0-9]+)?$";
264        }
265        else if ("email".equals(regexpType))
266        {
267            return "^([a-zA-Z0-9_\\.\\-\\+])+\\@(([a-zA-Z0-9\\-])+\\.)+([a-zA-Z0-9]{2,4})+$";
268        }
269        else if ("phone".equals(regexpType))
270        {
271            return "^(\\+?\\(?[0-9]{1,3}\\)?([\\s]?)(\\(0\\))?|0)([\\s]?)([0-9\\-\\+\\s]{4,})+$";
272        }
273        else if ("date".equals(regexpType))
274        {
275            return "^([12][0-9][0-9][0-9])-([01][0-9])-([0123][0-9])$";
276        }
277        else if ("time".equals(regexpType))
278        {
279            return "^([012][0-9]):([012345][0-9])$";
280        }
281        else if ("datetime".equals(regexpType))
282        {
283            return "^([12][0-9][0-9][0-9])-([01][0-9])-([0123][0-9]) ([012][0-9]):([012345][0-9])$";
284        }
285        
286        return null;
287    }
288    
289    /**
290     * Determines if the question is mandatory.
291     * @return true if the question is mandatory.
292     * @throws AmetysRepositoryException if an error occurs.
293     */
294    public boolean isMandatory() throws AmetysRepositoryException
295    {
296        try
297        {
298            return getNode().getProperty(__PROPERTY_MANDATORY).getBoolean();
299        }
300        catch (PathNotFoundException e)
301        {
302            return false;
303        }
304        catch (RepositoryException e)
305        {
306            throw new AmetysRepositoryException("Unable to get mandatory property", e);
307        }
308    }
309    
310    /**
311     * Set the mandatory.
312     * @param mandatory true for mandatory
313     * @throws AmetysRepositoryException if an error occurs.
314     */
315    public void setMandatory(boolean mandatory) throws AmetysRepositoryException
316    {
317        try
318        {
319            getNode().setProperty(__PROPERTY_MANDATORY, mandatory);
320        }
321        catch (RepositoryException e)
322        {
323            throw new AmetysRepositoryException("Unable to set mandatory property", e);
324        }
325    }
326    
327    /**
328     * Determines if the question has a "other" option
329     * @return true if the question has a "other" option
330     */
331    public boolean hasOtherOption ()
332    {
333        try
334        {
335            return getNode().getProperty(__PROPERTY_OTHER_OPTION).getBoolean();
336        }
337        catch (PathNotFoundException e)
338        {
339            return false;
340        }
341        catch (RepositoryException e)
342        {
343            throw new AmetysRepositoryException("Unable to get other option property", e);
344        }
345    }
346    
347    /**
348     * Set the "other" option. To be used only for single or multiple choice question
349     * @param other true to add the "other" option
350     */
351    public void setOtherOption (boolean other)
352    {
353        try
354        {
355            getNode().setProperty(__PROPERTY_OTHER_OPTION, other);
356        }
357        catch (RepositoryException e)
358        {
359            throw new AmetysRepositoryException("Unable to set other option property", e);
360        }
361    }
362    
363    /**
364     * Set options
365     * @param options the options as a Map of key, value
366     * @throws AmetysRepositoryException if an error occurs.
367     */
368    public void setOptions (Map<String, String> options) throws AmetysRepositoryException
369    {
370        try
371        {
372            Node optsNode = getNode().getNode(__NODE_NAME_OPTIONS);
373            NodeIterator itNode = optsNode.getNodes("ametys:*");
374
375            // Remove old options
376            while (itNode.hasNext())
377            {
378                Node node = itNode.nextNode();
379                node.remove();
380            }
381            
382            for (String name : options.keySet())
383            {
384                Node optNode = optsNode.addNode("ametys:" + name, "ametys:survey-option");
385                optNode.setProperty(__PROPERTY_TITLE, options.get(name));
386            }
387        }
388        catch (RepositoryException e)
389        {
390            throw new AmetysRepositoryException("Unable to set options property", e);
391        }
392    }
393    
394    /**
395     * Get options
396     * @return the options as a Map of key, value
397     * @throws AmetysRepositoryException if an error occurs.
398     */
399    public Map<String, String> getOptions () throws AmetysRepositoryException
400    {
401        try
402        {
403            Map<String, String> options = new LinkedHashMap<>();
404            
405            NodeIterator itNode = getNode().getNode(__NODE_NAME_OPTIONS).getNodes("ametys:*");
406            while (itNode.hasNext())
407            {
408                Node node = itNode.nextNode();
409                if (node.isNodeType("ametys:survey-option"))
410                {
411                    String value = node.getProperty(__PROPERTY_TITLE).getString();
412                    options.put(node.getName().substring("ametys:".length()), value);
413                }
414            }
415            
416            return options;
417        }
418        catch (RepositoryException e)
419        {
420            throw new AmetysRepositoryException("Unable to get options property", e);
421        }
422    }
423    
424    /**
425     * Set columns
426     * @param columns the columns as a Map of key, value
427     * @throws AmetysRepositoryException if an error occurs.
428     */
429    public void setColumns (Map<String, String> columns) throws AmetysRepositoryException
430    {
431        try
432        {
433            Node optsNode = getNode().getNode(__NODE_NAME_COLUMNS);
434            NodeIterator itNode = optsNode.getNodes("ametys:*");
435
436            // Remove old options
437            while (itNode.hasNext())
438            {
439                Node node = itNode.nextNode();
440                node.remove();
441            }
442            
443            for (String name : columns.keySet())
444            {
445                Node optNode = optsNode.addNode("ametys:" + name, "ametys:survey-option");
446                optNode.setProperty(__PROPERTY_TITLE, columns.get(name));
447            }
448        }
449        catch (RepositoryException e)
450        {
451            throw new AmetysRepositoryException("Unable to set columns property", e);
452        }
453    }
454    
455    /**
456     * Get columns
457     * @return the columns as a Map of key, value
458     * @throws AmetysRepositoryException if an error occurs.
459     */
460    public Map<String, String> getColumns () throws AmetysRepositoryException
461    {
462        try
463        {
464            Map<String, String> options = new LinkedHashMap<>();
465            
466            NodeIterator itNode = getNode().getNode(__NODE_NAME_COLUMNS).getNodes("ametys:*");
467            while (itNode.hasNext())
468            {
469                Node node = itNode.nextNode();
470                if (node.isNodeType("ametys:survey-option"))
471                {
472                    String value = node.getProperty(__PROPERTY_TITLE).getString();
473                    options.put(node.getName().substring("ametys:".length()), value);
474                }
475            }
476            
477            return options;
478        }
479        catch (RepositoryException e)
480        {
481            throw new AmetysRepositoryException("Unable to get columns property", e);
482        }
483    }
484    
485    /**
486     * Add a rule for branching
487     * @param option the chosen option
488     * @param ruleType the rule type
489     * @param page the page to jump or skip. Can be null.
490     * @throws AmetysRepositoryException if an error occurs.
491     */
492    public void addRules (String option, RuleType ruleType, String page) throws AmetysRepositoryException
493    {
494        try
495        {
496            if (!getNode().hasNode(__NODE_NAME_RULES))
497            {
498                getNode().addNode(__NODE_NAME_RULES, "ametys:survey-rules");
499            }
500            
501            Node rulesNode = getNode().getNode(__NODE_NAME_RULES);
502            
503            Node ruleNode = rulesNode.addNode(option, "ametys:survey-rule");
504            ruleNode.setProperty(__PROPERTY_RULE_TYPE, ruleType.name());
505            ruleNode.setProperty(__PROPERTY_RULE_OPTION, option);
506            if (ruleType == RuleType.JUMP || ruleType == RuleType.SKIP)
507            {
508                ruleNode.setProperty(__PROPERTY_RULE_PAGE, page);
509            }
510        }
511        catch (RepositoryException e)
512        {
513            throw new AmetysRepositoryException("Unable to add rule", e);
514        }
515    }
516    
517    /**
518     * Determines if a rule with given option exists
519     * @param option the option
520     * @return true if teh rule exists
521     * @throws AmetysRepositoryException if an error occurs.
522     */
523    public boolean hasRule (String option) throws AmetysRepositoryException
524    {
525        try
526        {
527            Node rulesNode = getNode().getNode(__NODE_NAME_RULES);
528            return rulesNode.hasNode(option);
529        }
530        catch (RepositoryException e)
531        {
532            throw new AmetysRepositoryException("Unable to check if rule exists", e);
533        }
534    }
535    
536    /**
537     * Delete a rule
538     * @param option the option to delete
539     * @throws AmetysRepositoryException  if an error occurs.
540     */
541    public void deleteRule (String option) throws AmetysRepositoryException
542    {
543        try
544        {
545            Node rulesNode = getNode().getNode(__NODE_NAME_RULES);
546            
547            if (rulesNode.hasNode(option))
548            {
549                rulesNode.getNode(option).remove();
550            }
551        }
552        catch (RepositoryException e)
553        {
554            throw new AmetysRepositoryException("Unable to delete rule", e);
555        }
556    }
557    
558    /**
559     * Get the rules
560     * @return the rules
561     * @throws AmetysRepositoryException if an error occurs.
562     */
563    public List<SurveyRule> getRules () throws AmetysRepositoryException
564    {
565        try
566        {
567            List<SurveyRule> rules = new LinkedList<>();
568            
569            NodeIterator itNode = getNode().getNode(__NODE_NAME_RULES).getNodes("*");
570            while (itNode.hasNext())
571            {
572                Node node = itNode.nextNode();
573                if (node.isNodeType("ametys:survey-rule"))
574                {
575                    String option = node.getProperty(__PROPERTY_RULE_OPTION).getString();
576                    RuleType type = RuleType.valueOf(node.getProperty(__PROPERTY_RULE_TYPE).getString());
577                    String page = null;
578                    if (node.hasProperty(__PROPERTY_RULE_PAGE))
579                    {
580                        page = node.getProperty(__PROPERTY_RULE_PAGE).getString();
581                    }
582                    rules.add(new SurveyRule(option, type, page));
583                }
584            }
585            
586            return rules;
587        }
588        catch (PathNotFoundException e)
589        {
590            return new LinkedList<>();
591        }
592        catch (RepositoryException e)
593        {
594            throw new AmetysRepositoryException("Unable to get rules property", e);
595        }
596    }
597    
598    /**
599     * Get the page to which this question belongs.
600     * @return the page to which this question belongs.
601     * @throws AmetysRepositoryException if a repository error occurs when retrieving the page attached to a survey
602     */
603    public SurveyPage getSurveyPage() throws AmetysRepositoryException
604    {
605        return getParent();
606    }
607    
608    /**
609     * Get the survey to which this question belongs.
610     * @return the survey to which this question belongs.
611     * @throws AmetysRepositoryException if a repository error occurs when retrieving a survey
612     */
613    public Survey getSurvey() throws AmetysRepositoryException
614    {
615        return getSurveyPage().getSurvey();
616    }
617    
618    @Override
619    public SurveyQuestion copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException
620    {
621        SurveyQuestion question = parent.createChild(name, "ametys:survey-question");
622        question.setLabel(getLabel());
623        question.setTitle(getTitle());
624        question.setMandatory(isMandatory());
625        question.setType(getType());
626        
627        String regExpType = getRegExpType();
628        if (StringUtils.isNotEmpty(regExpType))
629        {
630            question.setRegExpType(regExpType);
631        }
632        
633        if (getType().equals(QuestionType.MULTIPLE_CHOICE) || getType().equals(QuestionType.SINGLE_CHOICE))
634        {
635            question.setOtherOption(hasOtherOption());
636        }
637       
638        question.setOptions(getOptions());
639        question.setColumns(getColumns());
640        
641        for (SurveyRule rule : getRules())
642        {
643            question.addRules(rule.getOption(), rule.getType(), rule.getPage());
644        }
645        
646        copyPictureTo(question);
647        
648        return question;
649    }
650    
651    @Override
652    public SurveyQuestion copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException
653    {
654        return copyTo(parent, name);
655    }
656}