001/* 002 * Copyright 2014 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.thesaurus; 017 018import java.io.IOException; 019import java.io.InputStream; 020import java.nio.charset.StandardCharsets; 021import java.text.Normalizer; 022import java.util.ArrayList; 023import java.util.Collection; 024import java.util.HashMap; 025import java.util.LinkedHashSet; 026import java.util.List; 027import java.util.Map; 028import java.util.Set; 029import java.util.regex.Matcher; 030import java.util.regex.Pattern; 031 032import javax.jcr.Node; 033import javax.jcr.RepositoryException; 034import javax.jcr.Session; 035 036import org.apache.avalon.framework.component.Component; 037import org.apache.avalon.framework.logger.AbstractLogEnabled; 038import org.apache.avalon.framework.service.ServiceException; 039import org.apache.avalon.framework.service.ServiceManager; 040import org.apache.avalon.framework.service.Serviceable; 041import org.apache.commons.io.IOUtils; 042import org.apache.commons.lang3.ArrayUtils; 043import org.apache.commons.lang3.StringUtils; 044import org.apache.excalibur.source.Source; 045import org.apache.excalibur.source.SourceResolver; 046 047import org.ametys.cms.FilterNameHelper; 048import org.ametys.cms.repository.Content; 049import org.ametys.cms.repository.ModifiableContent; 050import org.ametys.cms.repository.WorkflowAwareContent; 051import org.ametys.cms.workflow.AbstractContentWorkflowComponent; 052import org.ametys.cms.workflow.ContentWorkflowHelper; 053import org.ametys.core.right.RightManager; 054import org.ametys.core.right.RightManager.RightResult; 055import org.ametys.core.ui.Callable; 056import org.ametys.core.user.CurrentUserProvider; 057import org.ametys.plugins.repository.AmetysObject; 058import org.ametys.plugins.repository.AmetysObjectIterable; 059import org.ametys.plugins.repository.AmetysObjectIterator; 060import org.ametys.plugins.repository.AmetysObjectResolver; 061import org.ametys.plugins.repository.AmetysRepositoryException; 062import org.ametys.plugins.repository.CollectionIterable; 063import org.ametys.plugins.repository.EmptyIterable; 064import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 065import org.ametys.plugins.repository.RepositoryConstants; 066import org.ametys.plugins.repository.TraversableAmetysObject; 067import org.ametys.plugins.repository.UnknownAmetysObjectException; 068import org.ametys.plugins.repository.jcr.JCRAmetysObject; 069import org.ametys.plugins.repository.lock.LockableAmetysObject; 070import org.ametys.plugins.repository.metadata.RichText; 071import org.ametys.plugins.thesaurus.content.ThesaurusItemContentType; 072import org.ametys.plugins.thesaurus.workflow.CreateTermFunction; 073 074import com.opensymphony.workflow.WorkflowException; 075 076/** 077 * DAO for manipulating thesaurus 078 * 079 */ 080public class ThesaurusDAO extends AbstractLogEnabled implements Serviceable, Component 081{ 082 /** The Avalon role */ 083 public static final String ROLE = ThesaurusDAO.class.getName(); 084 085 /** Metadata for related terms */ 086 public static final String METADATA_RELATED_TERMS = "relatedTerms"; 087 /** Metadata for specific terms */ 088 public static final String METADATA_SPECIFIC_TERMS = "specificTerms"; 089 /** Metadata for synonyms */ 090 public static final String METADATA_SYNONYMS = "synonyms"; 091 /** Metadata for applicationNote */ 092 public static final String METADATA_APPLICATION_NOTE = "applicationNote"; 093 /** Metadata for explanatoryNote */ 094 public static final String METADATA_EXPLANATORY_NOTE = "explanatoryNote"; 095 /** Name of root candidate node */ 096 public static final String ROOT_CANDIDATES_NODENAME = RepositoryConstants.NAMESPACE_PREFIX + ":candidates"; 097 098 private static final String __PLUGIN_NODE_NAME = "thesaurus"; 099 private static final String __ROOT_THESAURII_NODETYPE = RepositoryConstants.NAMESPACE_PREFIX + ":thesaurii"; 100 private static final String __TERM_WORKFLOW_NAME = "thesaurus"; 101 102 private static final Pattern __NUMERIC_PATTERN = Pattern.compile("^[0-9-_]+.*"); 103 104 /** Ametys resolver */ 105 protected AmetysObjectResolver _resolver; 106 /** Content workflow helper */ 107 protected ContentWorkflowHelper _contentWorkflowHelper; 108 /** Avalon source resolver */ 109 protected SourceResolver _srcResolver; 110 /** The right manager */ 111 protected RightManager _rightManager; 112 /** The current user provider */ 113 protected CurrentUserProvider _currentUserProvider; 114 115 @Override 116 public void service(ServiceManager manager) throws ServiceException 117 { 118 _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE); 119 _contentWorkflowHelper = (ContentWorkflowHelper) manager.lookup(ContentWorkflowHelper.ROLE); 120 _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE); 121 _rightManager = (RightManager) manager.lookup(RightManager.ROLE); 122 _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE); 123 } 124 125 /** 126 * Get the list of thesaurus 127 * @return the thesaurus 128 */ 129 public AmetysObjectIterable<Thesaurus> getThesaurii () 130 { 131 ModifiableTraversableAmetysObject rootNode = getRootNode(); 132 return rootNode.getChildren(); 133 } 134 135 /** 136 * Get microthesaurii of a thesaurus 137 * @param thesaurusId The id of thesaurus 138 * @return the microthesaurii 139 */ 140 public AmetysObjectIterable<MicroThesaurus> getMicrothesaurii (String thesaurusId) 141 { 142 try 143 { 144 Thesaurus thesaurus = _resolver.resolveById(thesaurusId); 145 return thesaurus.getChildren(); 146 } 147 catch (UnknownAmetysObjectException e) 148 { 149 getLogger().error("Unable to retrieve thesaurus of id '" + thesaurusId); 150 return new EmptyIterable<>(); 151 } 152 } 153 154 /** 155 * Get the parent microthesaurus 156 * @param term The term 157 * @return the parent microthesaurus 158 */ 159 public MicroThesaurus getParentMicrothesaurus (Content term) 160 { 161 AmetysObject parent = term.getParent(); 162 while (parent != null) 163 { 164 if (parent instanceof MicroThesaurus) 165 { 166 return (MicroThesaurus) parent; 167 } 168 parent = parent.getParent(); 169 } 170 return null; 171 } 172 173 /** 174 * Get a thesaurus by its unique name 175 * @param name The thesaurus' name 176 * @return the thesaurus or <code>null</code> if not found 177 */ 178 public Thesaurus getThesaurusByName (String name) 179 { 180 String xPathQuery = "//element(" + name + ", ametys:thesaurus)"; 181 182 AmetysObjectIterable<Thesaurus> contents = _resolver.query(xPathQuery); 183 AmetysObjectIterator<Thesaurus> it = contents.iterator(); 184 185 if (it.hasNext()) 186 { 187 return it.next(); 188 } 189 190 return null; 191 } 192 193 /** 194 * Get a microthesaurus by its unique name 195 * @param thesaurusName The thesaurusName' name 196 * @param microthesaurusName The microthesaurus' name 197 * @return the microthesaurus or <code>null</code> if not found 198 */ 199 public MicroThesaurus getMicrothesaurusByName (String thesaurusName, String microthesaurusName) 200 { 201 String xPathQuery = "//element(" + thesaurusName + ", ametys:thesaurus)/element(" + microthesaurusName + ", ametys:microthesaurus)"; 202 203 AmetysObjectIterable<MicroThesaurus> contents = _resolver.query(xPathQuery); 204 AmetysObjectIterator<MicroThesaurus> it = contents.iterator(); 205 206 if (it.hasNext()) 207 { 208 return it.next(); 209 } 210 211 return null; 212 } 213 214 /** 215 * Get the id of a microthesaurus by its unique name 216 * @param thesaurusName The thesaurusName' name 217 * @param microthesaurusName The microthesaurus' name 218 * @return the id of microthesaurus if found 219 */ 220 @Callable 221 public String getMicrothesaurusIdByName (String thesaurusName, String microthesaurusName) 222 { 223 MicroThesaurus microthesaurus = getMicrothesaurusByName(thesaurusName, microthesaurusName); 224 return microthesaurus != null ? microthesaurus.getId() : null; 225 } 226 227 /** 228 * Get the id of parent microthesaurus 229 * @param termId The id of term 230 * @return the id of parent microthesaurus 231 */ 232 @Callable 233 public Map<String, Object> getParentMicrothesaurus (String termId) 234 { 235 Content term = _resolver.resolveById(termId); 236 237 MicroThesaurus parentMicrothesaurus = getParentMicrothesaurus(term); 238 239 Map<String, Object> result = new HashMap<>(); 240 result.put("microthesaurusId", parentMicrothesaurus.getId()); 241 return result; 242 } 243 244 /** 245 * Get the parent thesaurus 246 * @param microThesaurus The microthesaurus 247 * @return the parent thesaurus 248 */ 249 public Thesaurus getParentThesaurus (MicroThesaurus microThesaurus) 250 { 251 return microThesaurus.getParent(); 252 } 253 254 /** 255 * Get the generic term or null 256 * @param term The term 257 * @return the parent term or null 258 */ 259 public Content getGenericTerm (Content term) 260 { 261 AmetysObject parent = term.getParent(); 262 while (!(parent instanceof MicroThesaurus)) 263 { 264 if (parent instanceof Content) 265 { 266 return (Content) parent; 267 } 268 parent = parent.getParent(); 269 } 270 return null; 271 } 272 273 /** 274 * Get direct child terms of a microthesaurus or a term 275 * @param parentId The parent Id 276 * @return The child terms 277 */ 278 public AmetysObjectIterable<Content> getChildTerms(String parentId) 279 { 280 TraversableAmetysObject parent = _resolver.resolveById(parentId); 281 return getChildTerms(parent); 282 } 283 284 /** 285 * Get child terms of a microthesaurus or a term 286 * @param parent The parent 287 * @return The child terms 288 */ 289 public AmetysObjectIterable<Content> getChildTerms(TraversableAmetysObject parent) 290 { 291 if (parent instanceof MicroThesaurus) 292 { 293 List<Content> contents = new ArrayList<>(); 294 for (AmetysObject child : parent.getChildren()) 295 { 296 if (child instanceof Content) 297 { 298 contents.add((Content) child); 299 } 300 } 301 return new CollectionIterable<>(contents); 302 } 303 else if (parent instanceof Content) 304 { 305 if (((Content) parent).getMetadataHolder().hasMetadata(METADATA_SPECIFIC_TERMS)) 306 { 307 TraversableAmetysObject specificTerms = ((Content) parent).getMetadataHolder().getObjectCollection(METADATA_SPECIFIC_TERMS); 308 return specificTerms.getChildren(); 309 } 310 311 return new EmptyIterable<>(); 312 } 313 else if (parent.getName().equals(ROOT_CANDIDATES_NODENAME)) 314 { 315 return parent.getChildren(); 316 } 317 318 throw new IllegalArgumentException("Object of id '" + parent.getId() + "' can not have child terms"); 319 } 320 321 /** 322 * Get the contents indexed with this term 323 * @param termId The term id 324 * @return The indexed contents 325 */ 326 public Set<Content> getIndexedContents (String termId) 327 { 328 Set<Content> indexedContents = new LinkedHashSet<>(); 329 330 Content term = _resolver.resolveById(termId); 331 332 Collection<Content> referencingContents = term.getReferencingContents(); 333 for (Content content : referencingContents) 334 { 335 String cType = content.getTypes()[0]; 336 if (!ThesaurusItemContentType.TERM_CONTENT_TYPE_ID.equals(cType)) 337 { 338 indexedContents.add(content); 339 } 340 } 341 342 return indexedContents; 343 } 344 345 /** 346 * Get the number of contents indexed with this term 347 * @param termId The term id 348 * @return The number of indexed contents 349 */ 350 public int getNumberOfIndexedContents (String termId) 351 { 352 Collection<Content> indexedContents = getIndexedContents(termId); 353 return indexedContents.size(); 354 } 355 356 /** 357 * Get the JSON representation of terms 358 * @param termIds The id of a terms 359 * @return A JSON map with informations properties 360 * @throws IOException when an error occurs while getting the term information 361 */ 362 @SuppressWarnings("unchecked") 363 @Callable 364 public Map<String, Object> getTermsInformation (List<String> termIds) throws IOException 365 { 366 Map<String, Object> terms = new HashMap<>(); 367 terms.put("terms", new ArrayList<>()); 368 369 for (String termId : termIds) 370 { 371 ((List<Object>) terms.get("terms")).add(getTermInformation(termId)); 372 } 373 return terms; 374 } 375 376 /** 377 * Get the JSON representation of term 378 * @param termId The id of a term 379 * @return A JSON map with informations properties 380 * @throws IOException if an error occurs when creating a {@link Source} 381 */ 382 @Callable 383 public Map<String, Object> getTermInformation (String termId) throws IOException 384 { 385 Map<String, Object> infos = new HashMap<>(); 386 Content term = _resolver.resolveById(termId); 387 388 infos.put("title", term.getTitle()); 389 infos.put("thesaurus", this.getParentThesaurus(this.getParentMicrothesaurus(term)).getLabel()); 390 infos.put("microthesaurus", this.getParentMicrothesaurus(term).getLabel()); 391 392 String cType = term.getTypes()[0]; 393 if (ThesaurusItemContentType.TERM_CONTENT_TYPE_ID.equals(cType)) 394 { 395 infos.put("type", "term"); 396 397 if (term.getMetadataHolder().hasMetadata(METADATA_SPECIFIC_TERMS)) 398 { 399 List<Map<String, Object>> specificTerms = new ArrayList<>(); 400 401 TraversableAmetysObject ts = term.getMetadataHolder().getObjectCollection(METADATA_SPECIFIC_TERMS); 402 for (AmetysObject child : ts.getChildren()) 403 { 404 if (child instanceof Content) 405 { 406 Map<String, Object> specificTerm = new HashMap<>(); 407 specificTerm.put("title", ((Content) child).getTitle()); 408 specificTerm.put("path", this.getTermPath((Content) child)); 409 specificTerms.add(specificTerm); 410 } 411 } 412 infos.put("specificTerms", specificTerms); 413 } 414 415 if (term.getMetadataHolder().hasMetadata(METADATA_RELATED_TERMS)) 416 { 417 List<Map<String, Object>> relatedTerms = new ArrayList<>(); 418 419 String[] ids = term.getMetadataHolder().getStringArray(ThesaurusDAO.METADATA_RELATED_TERMS); 420 for (String id : ids) 421 { 422 Content ta = _resolver.resolveById(id); 423 Map<String, Object> relatedTerm = new HashMap<>(); 424 relatedTerm.put("title", ta.getTitle()); 425 relatedTerm.put("path", this.getTermPath(ta)); 426 relatedTerms.add(relatedTerm); 427 } 428 infos.put("relatedTerms", relatedTerms); 429 } 430 431 if (term.getMetadataHolder().hasMetadata(ThesaurusDAO.METADATA_SYNONYMS)) 432 { 433 String[] synonyms = term.getMetadataHolder().getStringArray(ThesaurusDAO.METADATA_SYNONYMS); 434 infos.put("synonyms", StringUtils.join(synonyms, ", ")); 435 } 436 437 if (term.getMetadataHolder().hasMetadata(ThesaurusDAO.METADATA_EXPLANATORY_NOTE)) 438 { 439 RichText explanatoryNote = term.getMetadataHolder().getRichText(ThesaurusDAO.METADATA_EXPLANATORY_NOTE); 440 441 InputStream docbookIS = null; 442 try 443 { 444 docbookIS = explanatoryNote.getInputStream(); 445 HashMap<String, Object> params = new HashMap<>(); 446 params.put("source", docbookIS); 447 448 Source source = _srcResolver.resolveURI("cocoon://_plugins/thesaurus/convert/docbook2html", null, params); 449 450 try (InputStream htmlIS = source.getInputStream()) 451 { 452 infos.put("explanatoryNote", IOUtils.toString(htmlIS, StandardCharsets.UTF_8)); 453 } 454 } 455 catch (IOException ex) 456 { 457 throw new RuntimeException(ex); 458 } 459 } 460 461 if (term.getMetadataHolder().hasMetadata(ThesaurusDAO.METADATA_APPLICATION_NOTE)) 462 { 463 RichText applicationNote = term.getMetadataHolder().getRichText(ThesaurusDAO.METADATA_APPLICATION_NOTE); 464 465 InputStream docbookIS = null; 466 try 467 { 468 docbookIS = applicationNote.getInputStream(); 469 HashMap<String, Object> params = new HashMap<>(); 470 params.put("source", docbookIS); 471 472 Source source = _srcResolver.resolveURI("cocoon://_plugins/thesaurus/convert/docbook2html", null, params); 473 474 try (InputStream htmlIS = source.getInputStream()) 475 { 476 infos.put("applicationNote", IOUtils.toString(htmlIS, StandardCharsets.UTF_8)); 477 } 478 } 479 catch (IOException ex) 480 { 481 throw new RuntimeException(ex); 482 } 483 } 484 } 485 else if (ThesaurusItemContentType.CANDIDAT_CONTENT_TYPE_ID.equals(cType)) 486 { 487 infos.put("type", "candidate"); 488 infos.put("comment", term.getMetadataHolder().getString("comment", "")); 489 } 490 491 return infos; 492 } 493 494 /** 495 * Get the JSON representation of terms 496 * @param ids The ids of terms 497 * @return A JSON map with term's properties 498 */ 499 @SuppressWarnings("unchecked") 500 @Callable 501 public Map<String, Object> termsToJSON (List<String> ids) 502 { 503 Map<String, Object> terms = new HashMap<>(); 504 terms.put("terms", new ArrayList<>()); 505 506 for (String id : ids) 507 { 508 Map<String, Object> infos = new HashMap<>(); 509 510 Content term = _resolver.resolveById(id); 511 512 infos.put("id", term.getId()); 513 infos.put("name", term.getName()); 514 infos.put("title", term.getTitle()); 515 infos.put("lang", term.getLanguage()); 516 infos.put("path", getTermPath(term)); 517 infos.put("creator", term.getCreator()); 518 infos.put("lastContributor", term.getLastContributor()); 519 infos.put("creationDate", term.getCreationDate()); 520 infos.put("lastModified", term.getLastModified()); 521 522 MicroThesaurus microthesaurus = getParentMicrothesaurus(term); 523 infos.put("microthesaurusId", microthesaurus.getId()); 524 525 Thesaurus thesaurus = getParentThesaurus(microthesaurus); 526 infos.put("thesaurusId", thesaurus.getId()); 527 528 if (term instanceof ModifiableContent) 529 { 530 infos.put("isModifiable", true); 531 } 532 533 if (term instanceof LockableAmetysObject) 534 { 535 LockableAmetysObject lockableContent = (LockableAmetysObject) term; 536 if (lockableContent.isLocked()) 537 { 538 infos.put("locked", true); 539 infos.put("lockOwner", lockableContent.getLockOwner()); 540 } 541 } 542 543 if (ThesaurusItemContentType.TERM_CONTENT_TYPE_ID.equals(term.getTypes()[0])) 544 { 545 Content genericTerm = getGenericTerm(term); 546 if (genericTerm != null) 547 { 548 infos.put("parentId", genericTerm.getId()); 549 } 550 } 551 else if (ThesaurusItemContentType.CANDIDAT_CONTENT_TYPE_ID.equals(term.getTypes()[0])) 552 { 553 infos.put("parentId", term.getParent().getId()); 554 555 String comment = term.getMetadataHolder().getString("comment", ""); 556 infos.put("comment", comment); 557 } 558 559 ((List<Object>) terms.get("terms")).add(infos); 560 } 561 562 return terms; 563 } 564 565 /** 566 * Creates a microthesaurus 567 * @param label The label 568 * @return The id and label of the created microthesaurus 569 */ 570 @Callable 571 public Map<String, Object> createThesaurus (String label) 572 { 573 ModifiableTraversableAmetysObject rootNode = getRootNode(); 574 575 if (_rightManager.hasRight(_currentUserProvider.getUser(), "Thesaurus_Rights_EditThesaurus", rootNode) != RightResult.RIGHT_ALLOW) 576 { 577 String errorMessage = "User " + _currentUserProvider.getUser() + " try to create thesaurus with no sufficient rights"; 578 getLogger().error(errorMessage); 579 throw new IllegalStateException(errorMessage); 580 } 581 582 String originalName = FilterNameHelper.filterName(label); 583 584 // Find unique name 585 int index = 2; 586 String name = originalName; 587 while (rootNode.hasChild(name)) 588 { 589 name = originalName + "_" + (index++); 590 } 591 592 Thesaurus thesaurus = rootNode.createChild(name, ThesaurusFactory.THESAURUS_NODETYPE); 593 thesaurus.setLabel(label); 594 595 rootNode.saveChanges(); 596 597 Map<String, Object> result = new HashMap<>(); 598 result.put("id", thesaurus.getId()); 599 result.put("label", thesaurus.getLabel()); 600 601 return result; 602 } 603 604 /** 605 * Updates a thesaurus 606 * @param thesaurusId The id of microthesaurus 607 * @param label The new label 608 * @return The id and label of the updated microthesaurus 609 */ 610 @Callable 611 public Map<String, Object> updateThesaurus (String thesaurusId, String label) 612 { 613 Thesaurus thesaurus = _resolver.resolveById(thesaurusId); 614 615 if (_rightManager.hasRight(_currentUserProvider.getUser(), "Thesaurus_Rights_EditThesaurus", thesaurus) != RightResult.RIGHT_ALLOW) 616 { 617 String errorMessage = "User " + _currentUserProvider.getUser() + " try to edit thesaurus of id " + thesaurusId + " with no sufficient rights"; 618 getLogger().error(errorMessage); 619 throw new IllegalStateException(errorMessage); 620 } 621 622 thesaurus.setLabel(label); 623 thesaurus.saveChanges(); 624 625 Map<String, Object> result = new HashMap<>(); 626 result.put("id", thesaurus.getId()); 627 result.put("label", thesaurus.getLabel()); 628 629 return result; 630 } 631 632 /** 633 * Deletes a thesaurus 634 * @param thesaurusId The id of thesaurus 635 * @return The id of the deleted thesaurus 636 */ 637 @Callable 638 public Map<String, Object> deleteThesaurus (String thesaurusId) 639 { 640 Map<String, Object> result = new HashMap<>(); 641 642 Thesaurus thesaurus = _resolver.resolveById(thesaurusId); 643 644 if (_rightManager.hasRight(_currentUserProvider.getUser(), "Thesaurus_Rights_EditThesaurus", thesaurus) != RightResult.RIGHT_ALLOW) 645 { 646 String errorMessage = "User " + _currentUserProvider.getUser() + " try to delete thesaurus of id " + thesaurusId + " with no sufficient rights"; 647 getLogger().error(errorMessage); 648 throw new IllegalStateException(errorMessage); 649 } 650 651 result.put("id", thesaurus.getId()); 652 653 // Microthesaurii must be deleted first. 654 AmetysObjectIterable<MicroThesaurus> microthesaurii = getMicrothesaurii(thesaurusId); 655 AmetysObjectIterator<MicroThesaurus> it = microthesaurii.iterator(); 656 657 while (it.hasNext()) 658 { 659 Map<String, Object> sResult = deleteMicrothesaurus(it.next().getId()); 660 if (sResult.containsKey("hasReferences")) 661 { 662 result.put("hasReferences", true); 663 return result; 664 } 665 } 666 667 thesaurus.remove(); 668 thesaurus.saveChanges(); 669 670 return result; 671 } 672 673 /** 674 * Get the label of thesaurus 675 * @param thesaurusId The id of thesaurus 676 * @return The id and label of the thesaurus 677 */ 678 @Callable 679 public Map<String, Object> getThesaurusLabel (String thesaurusId) 680 { 681 Map<String, Object> result = new HashMap<>(); 682 try 683 { 684 Thesaurus thesaurus = _resolver.resolveById(thesaurusId); 685 686 result.put("id", thesaurus.getId()); 687 result.put("label", thesaurus.getLabel()); 688 } 689 catch (UnknownAmetysObjectException e) 690 { 691 getLogger().error("Thesaurus of id '" + thesaurusId + "' does not exist anymore", e); 692 result.put("error", "not-found"); 693 } 694 695 return result; 696 } 697 698 /** 699 * Creates a microthesaurus 700 * @param label The label 701 * @param thesaurusId The id of parent thesaurus 702 * @return The id and label of the created microthesaurus 703 */ 704 @Callable 705 public Map<String, Object> createMicrothesaurus (String label, String thesaurusId) 706 { 707 ModifiableTraversableAmetysObject rootNode = getRootNode(); 708 709 Thesaurus thesaurus = _resolver.resolveById(thesaurusId); 710 711 String originalName = FilterNameHelper.filterName(label); 712 713 // Find unique name 714 int index = 2; 715 String name = originalName; 716 while (thesaurus.hasChild(name)) 717 { 718 name = originalName + "_" + (index++); 719 } 720 721 if (_rightManager.hasRight(_currentUserProvider.getUser(), "Thesaurus_Rights_EditMicrothesaurus", thesaurus) != RightResult.RIGHT_ALLOW) 722 { 723 String errorMessage = "User " + _currentUserProvider.getUser() + " try to create a microthesaurus with no sufficient rights"; 724 getLogger().error(errorMessage); 725 throw new IllegalStateException(errorMessage); 726 } 727 728 MicroThesaurus mt = thesaurus.createChild(name, MicroThesaurusFactory.MICROTHESAURUS_NODETYPE); 729 mt.setLabel(label); 730 731 rootNode.saveChanges(); 732 733 Map<String, Object> result = new HashMap<>(); 734 result.put("id", mt.getId()); 735 result.put("label", mt.getLabel()); 736 result.put("thesaurusId", mt.getParent().getId()); 737 738 return result; 739 } 740 741 /** 742 * Updates a microthesaurus 743 * @param microthesaurusId The id of microthesaurus 744 * @param label The label 745 * @return The id and label of the updated microthesaurus 746 */ 747 @Callable 748 public Map<String, Object> updateMicrothesaurus (String microthesaurusId, String label) 749 { 750 MicroThesaurus microThesaurus = _resolver.resolveById(microthesaurusId); 751 752 if (_rightManager.hasRight(_currentUserProvider.getUser(), "Thesaurus_Rights_EditMicrothesaurus", microThesaurus) != RightResult.RIGHT_ALLOW) 753 { 754 String errorMessage = "User " + _currentUserProvider.getUser() + " try to edit microthesaurus of id '" + microthesaurusId + "' with no sufficient rights"; 755 getLogger().error(errorMessage); 756 throw new IllegalStateException(errorMessage); 757 } 758 759 microThesaurus.setLabel(label); 760 microThesaurus.saveChanges(); 761 762 Map<String, Object> result = new HashMap<>(); 763 result.put("id", microThesaurus.getId()); 764 result.put("label", microThesaurus.getLabel()); 765 result.put("thesaurusId", microThesaurus.getParent().getId()); 766 767 return result; 768 } 769 770 /** 771 * Deletes a microthesaurus 772 * @param microthesaurusId The id of microthesaurus 773 * @return The id of the deleted microthesaurus 774 */ 775 @Callable 776 public Map<String, Object> deleteMicrothesaurus(String microthesaurusId) 777 { 778 Map<String, Object> result = new HashMap<>(); 779 MicroThesaurus microThesaurus = _resolver.resolveById(microthesaurusId); 780 781 if (_rightManager.hasRight(_currentUserProvider.getUser(), "Thesaurus_Rights_EditMicrothesaurus", microThesaurus) != RightResult.RIGHT_ALLOW) 782 { 783 String errorMessage = "User " + _currentUserProvider.getUser() + " try to delete microthesaurus of id '" + microthesaurusId + "' with no sufficient rights"; 784 getLogger().error(errorMessage); 785 throw new IllegalStateException(errorMessage); 786 } 787 788 result.put("id", microThesaurus.getId()); 789 result.put("thesaurusId", microThesaurus.getParent().getId()); 790 791 // First check references 792 AmetysObjectIterable<Content> terms = getChildTerms(microthesaurusId); 793 AmetysObjectIterator<Content> it = terms.iterator(); 794 while (it.hasNext()) 795 { 796 if (!_checkReferenced(it.next())) 797 { 798 result.put("hasReferences", true); 799 return result; 800 } 801 } 802 803 if (microThesaurus.hasChild(ThesaurusDAO.ROOT_CANDIDATES_NODENAME)) 804 { 805 TraversableAmetysObject rootCandidates = microThesaurus.getChild(ThesaurusDAO.ROOT_CANDIDATES_NODENAME); 806 AmetysObjectIterable<Content> candidates = getChildTerms(rootCandidates.getId()); 807 AmetysObjectIterator<Content> candidatesIt = candidates.iterator(); 808 809 while (candidatesIt.hasNext()) 810 { 811 if (!_checkReferenced(candidatesIt.next())) 812 { 813 result.put("hasReferences", true); 814 return result; 815 } 816 } 817 } 818 819 microThesaurus.remove(); 820 microThesaurus.saveChanges(); 821 822 return result; 823 } 824 825 /** 826 * Creates a new candidate 827 * @param label The label of candidate 828 * @param comment Comment on candidate 829 * @param lang The language code 830 * @param microthesaurusId The parent microthesaurus 831 * @return The id of created content 832 * @throws WorkflowException if an error occurs in the created candidate's workflow 833 */ 834 @Callable 835 public Map<String, Object> createCandidate (String label, String comment, String lang, String microthesaurusId) throws WorkflowException 836 { 837 return createCandidate(label, comment, lang, microthesaurusId, false); 838 } 839 840 /** 841 * Creates a new candidate 842 * @param label The label of candidate 843 * @param comment Comment on candidate 844 * @param lang The language code 845 * @param microthesaurusId The parent microthesaurus 846 * @param disableIndexation When true, disable the indexation 847 * @return The id of created content 848 * @throws WorkflowException if an error occurs in the created candidate's workflow 849 */ 850 @Callable 851 public Map<String, Object> createCandidate (String label, String comment, String lang, String microthesaurusId, boolean disableIndexation) throws WorkflowException 852 { 853 MicroThesaurus microthesaurus = _resolver.resolveById(microthesaurusId); 854 855 ModifiableTraversableAmetysObject rootCandidates = _getOrCreateNode(microthesaurus, ROOT_CANDIDATES_NODENAME, "ametys:unstructured"); 856 857 Map<String, Object> inputs = new HashMap<>(); 858 if (disableIndexation) 859 { 860 inputs.put(CreateTermFunction.DISABLE_INDEXATION, Boolean.TRUE); 861 } 862 inputs.put(CreateTermFunction.PARENT_MT_ID_KEY, rootCandidates.getId()); 863 Map<String, Object> result = _contentWorkflowHelper.createContent(__TERM_WORKFLOW_NAME, 1, _getContentName(label), label, new String[]{ThesaurusItemContentType.CANDIDAT_CONTENT_TYPE_ID}, new String[0], lang, null, null, inputs); 864 865 WorkflowAwareContent content = (WorkflowAwareContent) result.get(AbstractContentWorkflowComponent.CONTENT_KEY); 866 867 if (StringUtils.isNotEmpty(comment)) 868 { 869 content.getMetadataHolder().setMetadata("comment", comment); 870 content.saveChanges(); 871 } 872 873 Map<String, Object> jsonMap = new HashMap<>(); 874 jsonMap.put("id", content.getId()); 875 jsonMap.put("parentId", content.getParent().getId()); 876 jsonMap.put("microthesaurusId", microthesaurusId); 877 jsonMap.put("thesaurusId", microthesaurus.getParent().getId()); 878 879 return jsonMap; 880 } 881 882 /** 883 * Edits a candidate 884 * @param id The id of candidate 885 * @param label The label 886 * @param comment The comment 887 * @return The result 888 */ 889 @Callable 890 public Map<String, Object> editCandidate (String id, String label, String comment) 891 { 892 ModifiableContent content = _resolver.resolveById(id); 893 content.setTitle(label); 894 content.getMetadataHolder().setMetadata("comment", comment); 895 896 content.saveChanges(); 897 898 Map<String, Object> result = new HashMap<>(); 899 result.put("id", content.getId()); 900 result.put("label", content.getTitle()); 901 902 return result; 903 } 904 905 /** 906 * Creates a new term of micro-thesaurus 907 * @param label The label 908 * @param lang The language code 909 * @param parentId The parent id in micro-thesaurus 910 * @return The id of created content 911 * @throws WorkflowException if an error occurs in the created terms's workflow 912 */ 913 @Callable 914 public Map<String, Object> createTerm (String label, String lang, String parentId) throws WorkflowException 915 { 916 return createTerm(label, lang, parentId, false); 917 } 918 919 /** 920 * Creates a new term of micro-thesaurus 921 * @param label The label 922 * @param lang The language code 923 * @param parentId The parent id in micro-thesaurus 924 * @param disableIndexation true not to index the term, false otherwise 925 * @return The id of created content 926 * @throws WorkflowException if an error occurs in the created terms's workflow 927 */ 928 @Callable 929 public Map<String, Object> createTerm (String label, String lang, String parentId, boolean disableIndexation) throws WorkflowException 930 { 931 Map<String, Object> result = null; 932 933 Map<String, Object> inputs = new HashMap<>(); 934 if (disableIndexation) 935 { 936 inputs.put(CreateTermFunction.DISABLE_INDEXATION, Boolean.TRUE); 937 } 938 939 AmetysObject parent = _resolver.resolveById(parentId); 940 if (parent instanceof MicroThesaurus) 941 { 942 inputs.put(CreateTermFunction.PARENT_MT_ID_KEY, parentId); 943 result = _contentWorkflowHelper.createContent(__TERM_WORKFLOW_NAME, 1, _getContentName(label), label, new String[]{ThesaurusItemContentType.TERM_CONTENT_TYPE_ID}, new String[0], lang, null, null, inputs); 944 } 945 else 946 { 947 result = _contentWorkflowHelper.createContent(__TERM_WORKFLOW_NAME, 1, _getContentName(label), label, new String[]{ThesaurusItemContentType.TERM_CONTENT_TYPE_ID}, new String[0], lang, parentId, METADATA_SPECIFIC_TERMS, inputs); 948 } 949 950 WorkflowAwareContent content = (WorkflowAwareContent) result.get(AbstractContentWorkflowComponent.CONTENT_KEY); 951 MicroThesaurus parentMicrothesaurus = getParentMicrothesaurus(content); 952 Content genericTerm = getGenericTerm(content); 953 954 Map<String, Object> jsonMap = new HashMap<>(); 955 jsonMap.put("id", content.getId()); 956 jsonMap.put("parentId", genericTerm != null ? genericTerm.getId() : parentMicrothesaurus.getId()); 957 jsonMap.put("microthesaurusId", parentMicrothesaurus.getId()); 958 jsonMap.put("thesaurusId", parentMicrothesaurus.getParent().getId()); 959 960 return jsonMap; 961 } 962 963 /** 964 * Move a term 965 * @param term the term to move 966 * @param parent The parent ametys object, to where the term will be moved 967 * @throws AmetysRepositoryException if an error occurs while manipulating the repository 968 */ 969 public void moveTerm(Content term, TraversableAmetysObject parent) throws AmetysRepositoryException 970 { 971 try 972 { 973 Node termNode = null; 974 Node parentNode = null; 975 Session jcrSession = null; 976 977 // Src 978 if (term instanceof JCRAmetysObject) 979 { 980 termNode = ((JCRAmetysObject) term).getNode(); 981 jcrSession = termNode.getSession(); 982 } 983 else 984 { 985 throw new AmetysRepositoryException("Unable to move the term '" + term.getId() + "'. A JCRAmetysObject is expected"); 986 } 987 988 // Dst 989 if (parent instanceof MicroThesaurus) 990 { 991 parentNode = ((JCRAmetysObject) parent).getNode(); 992 } 993 else if (parent instanceof ModifiableContent) 994 { 995 TraversableAmetysObject specificTerms = ((ModifiableContent) parent).getMetadataHolder().getObjectCollection(METADATA_SPECIFIC_TERMS, true); 996 997 if (specificTerms instanceof JCRAmetysObject) 998 { 999 parentNode = ((JCRAmetysObject) specificTerms).getNode(); 1000 } 1001 else 1002 { 1003 throw new AmetysRepositoryException("Unable to move the term '" + term.getId() + "'. Parent specific term node should be a should be a JCRAmetysObject. Parent id : '" + parent.getId() + "'."); 1004 } 1005 } 1006 else 1007 { 1008 throw new AmetysRepositoryException("Unable to move the term '" + term.getId() + "'. Parent should be a MicroThesaurus or a ModifiableContent : '" + parent.getId() + "'."); 1009 } 1010 1011 String dstAbsPath = parentNode.getPath(); 1012 String originalName = termNode.getName(); 1013 String name = originalName; 1014 1015 // Find unused name on new parent node 1016 int index = 1; 1017 while (jcrSession.getRootNode().hasNode(dstAbsPath.substring(1) + "/" + name)) 1018 { 1019 name = originalName + "-" + index++; 1020 } 1021 1022 // Move node 1023 jcrSession.move(termNode.getPath(), dstAbsPath + "/" + name); 1024 1025 } 1026 catch (RepositoryException e) 1027 { 1028 throw new AmetysRepositoryException(String.format("Unable to move term '%s' to parent '%s'", term.getId(), parent.getId()), e); 1029 } 1030 } 1031 1032 /** 1033 * Retrieves the content name given the desired title 1034 * @param title the content's title 1035 * @return The content name 1036 */ 1037 protected String _getContentName (String title) 1038 { 1039 Matcher m = __NUMERIC_PATTERN.matcher(title); 1040 if (m.matches()) 1041 { 1042 return "x" + title; 1043 } 1044 return title; 1045 } 1046 1047 /** 1048 * Determines if the content is a candidate 1049 * @param content The content 1050 * @return <code>true</code> if it is a candidate 1051 */ 1052 public boolean isCandidate (Content content) 1053 { 1054 return ThesaurusItemContentType.CANDIDAT_CONTENT_TYPE_ID.equals(content.getTypes()[0]); 1055 } 1056 1057 private boolean _checkReferenced (Content term) 1058 { 1059 if (term instanceof TraversableAmetysObject) 1060 { 1061 AmetysObjectIterable<Content> terms = getChildTerms((TraversableAmetysObject) term); 1062 AmetysObjectIterator<Content> it = terms.iterator(); 1063 while (it.hasNext()) 1064 { 1065 if (!_checkReferenced(it.next())) 1066 { 1067 return false; 1068 } 1069 } 1070 } 1071 1072 Collection<Content> referencingContents = term.getReferencingContents(); 1073 for (Content content : referencingContents) 1074 { 1075 if (!ArrayUtils.contains(content.getTypes(), ThesaurusItemContentType.TERM_CONTENT_TYPE_ID)) 1076 { 1077 // The term is referenced by a content 1078 return false; 1079 } 1080 } 1081 1082 // The term is not referenced or referenced by other term(s) 1083 return true; 1084 } 1085 1086 /** 1087 * Returns paths of terms ids 1088 * @param termIds the list of terms ids 1089 * @return paths 1090 */ 1091 @Callable 1092 public Map<String, Object> getTermsPaths(List<String> termIds) 1093 { 1094 Map<String, Object> result = new HashMap<>(); 1095 List<String> paths = new ArrayList<>(); 1096 1097 for (String termId : termIds) 1098 { 1099 ModifiableContent term = _resolver.resolveById(termId); 1100 paths.add(getTermPath(term)); 1101 } 1102 1103 result.put("paths", paths); 1104 1105 return result; 1106 } 1107 1108 /** 1109 * Returns the path of this term in its hierarchy<br> 1110 * @param term The content term 1111 * @return the path of this term in its hierarchy. 1112 * @throws AmetysRepositoryException if an error occurs. 1113 */ 1114 public String getTermPath (Content term) 1115 { 1116 Content parent = getGenericTerm(term); 1117 1118 if (parent != null) 1119 { 1120 return getTermPath(parent) + "/" + term.getName(); 1121 } 1122 else 1123 { 1124 if (term.getTypes()[0].contains("candidate")) 1125 { 1126 return "ametys:candidates/" + term.getName(); 1127 } 1128 return term.getName(); 1129 } 1130 } 1131 1132 /** 1133 * Get the root plugin storage object. 1134 * @return the root plugin storage object. 1135 * @throws AmetysRepositoryException if a repository error occurs. 1136 */ 1137 public ModifiableTraversableAmetysObject getRootNode() throws AmetysRepositoryException 1138 { 1139 try 1140 { 1141 ModifiableTraversableAmetysObject pluginsNode = _resolver.resolveByPath("/ametys:plugins"); 1142 1143 ModifiableTraversableAmetysObject pluginNode = _getOrCreateNode(pluginsNode, __PLUGIN_NODE_NAME, "ametys:unstructured"); 1144 1145 return _getOrCreateNode(pluginNode, "ametys:thesaurii", __ROOT_THESAURII_NODETYPE); 1146 } 1147 catch (AmetysRepositoryException e) 1148 { 1149 throw new AmetysRepositoryException("Unable to get the thesaurus root node", e); 1150 } 1151 } 1152 1153 /** 1154 * Get or create a node 1155 * @param parentNode the parent node 1156 * @param nodeName the name of the node 1157 * @param nodeType the type of the node 1158 * @return The retrieved or created node 1159 * @throws AmetysRepositoryException if an error occurs when manipulating the repository 1160 */ 1161 protected ModifiableTraversableAmetysObject _getOrCreateNode(ModifiableTraversableAmetysObject parentNode, String nodeName, String nodeType) throws AmetysRepositoryException 1162 { 1163 ModifiableTraversableAmetysObject definitionsNode; 1164 if (parentNode.hasChild(nodeName)) 1165 { 1166 definitionsNode = parentNode.getChild(nodeName); 1167 } 1168 else 1169 { 1170 definitionsNode = parentNode.createChild(nodeName, nodeType); 1171 parentNode.saveChanges(); 1172 } 1173 return definitionsNode; 1174 } 1175 1176 /** 1177 * Get the path of node which match filter regexp 1178 * @param value the value to match 1179 * @param parentId the parent node id 1180 * @return the matching paths 1181 */ 1182 @Callable 1183 public Map<String, Object> filterTermsByRegExp (String value, String parentId) 1184 { 1185 TraversableAmetysObject parent = _resolver.resolveById(parentId); 1186 1187 String toMatch = Normalizer.normalize(value.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", "").trim(); 1188 1189 Map<String, Object> result = new HashMap<>(); 1190 1191 List<Map<String, Object>> matchingPaths = new ArrayList<>(); 1192 _getMatchingTerm(toMatch, parent, matchingPaths); 1193 1194 result.put("paths", matchingPaths); 1195 return result; 1196 } 1197 1198 /** 1199 * Get paths of term which match filter regexp 1200 * @param value the value to match 1201 * @param ao the ametys object 1202 * @param matchingPaths the matching paths 1203 */ 1204 private void _getMatchingTerm (String value, TraversableAmetysObject ao, List<Map<String, Object>> matchingPaths) 1205 { 1206 if (ao instanceof Content) 1207 { 1208 Content content = (Content) ao; 1209 1210 String normalizedName = Normalizer.normalize(content.getTitle().toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", ""); 1211 1212 if (normalizedName.contains(value)) 1213 { 1214 Map<String, Object> matchingPath = new HashMap<>(); 1215 matchingPath.put("path", getTermPath(content)); 1216 matchingPath.put("matchSynonyms", false); 1217 matchingPaths.add(matchingPath); 1218 } 1219 else if (content.getMetadataHolder().hasMetadata(METADATA_SYNONYMS)) 1220 { 1221 String[] synonyms = content.getMetadataHolder().getStringArray(METADATA_SYNONYMS, new String[0]); 1222 for (String synonym : synonyms) 1223 { 1224 String normalizedSynonym = Normalizer.normalize(synonym.toLowerCase(), Normalizer.Form.NFD).replaceAll("[\\p{InCombiningDiacriticalMarks}]", ""); 1225 1226 if (normalizedSynonym.contains(value)) 1227 { 1228 Map<String, Object> matchingPath = new HashMap<>(); 1229 matchingPath.put("path", getTermPath(content)); 1230 matchingPath.put("matchSynonyms", true); 1231 matchingPaths.add(matchingPath); 1232 break; 1233 } 1234 } 1235 } 1236 1237 if (content.getMetadataHolder().hasMetadata(METADATA_SPECIFIC_TERMS)) 1238 { 1239 TraversableAmetysObject specificTerms = content.getMetadataHolder().getObjectCollection(METADATA_SPECIFIC_TERMS); 1240 1241 for (AmetysObject child : specificTerms.getChildren()) 1242 { 1243 _getMatchingTerm(value, (TraversableAmetysObject) child, matchingPaths); 1244 } 1245 } 1246 } 1247 else 1248 { 1249 AmetysObjectIterable< ? extends TraversableAmetysObject> children = ao.getChildren(); 1250 for (TraversableAmetysObject child : children) 1251 { 1252 _getMatchingTerm (value, child, matchingPaths); 1253 } 1254 } 1255 } 1256}