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}