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.explorer.resources.jcr; 017 018import java.io.InputStream; 019import java.util.Calendar; 020import java.util.Date; 021import java.util.GregorianCalendar; 022import java.util.List; 023import java.util.Set; 024 025import javax.jcr.Binary; 026import javax.jcr.Node; 027import javax.jcr.NodeIterator; 028import javax.jcr.RepositoryException; 029import javax.jcr.Value; 030import javax.jcr.version.VersionHistory; 031 032import org.apache.commons.lang.StringUtils; 033 034import org.ametys.core.user.UserIdentity; 035import org.ametys.plugins.explorer.ExplorerNode; 036import org.ametys.plugins.explorer.resources.ModifiableResource; 037import org.ametys.plugins.explorer.resources.Resource; 038import org.ametys.plugins.explorer.threads.Thread; 039import org.ametys.plugins.explorer.threads.jcr.JCRThread; 040import org.ametys.plugins.explorer.threads.jcr.JCRThreadFactory; 041import org.ametys.plugins.repository.AmetysObject; 042import org.ametys.plugins.repository.AmetysRepositoryException; 043import org.ametys.plugins.repository.CopiableAmetysObject; 044import org.ametys.plugins.repository.ModifiableTraversableAmetysObject; 045import org.ametys.plugins.repository.RepositoryConstants; 046import org.ametys.plugins.repository.dublincore.DCMITypes; 047import org.ametys.plugins.repository.jcr.DefaultLockableAmetysObject; 048import org.ametys.plugins.repository.jcr.DublinCoreHelper; 049import org.ametys.plugins.repository.jcr.NodeTypeHelper; 050import org.ametys.plugins.repository.tag.TaggableAmetysObjectHelper; 051 052/** 053 * Default implementation of an {@link Resource}, backed by a JCR node.<br> 054 * @param <F> the actual type of factory. 055 */ 056public class JCRResource<F extends JCRResourceFactory> extends DefaultLockableAmetysObject<F> implements ModifiableResource, CopiableAmetysObject 057{ 058 /** The name of node holding the creator */ 059 public static final String CREATOR_NODE_NAME = "creator"; 060 /** Constants for lastModified Metadata */ 061 public static final String CREATION_DATE = "creationDate"; 062 /** The name of node holding the last contributor */ 063 public static final String CONTRIBUTOR_NODE_NAME = "contributor"; 064 065 /** 066 * Creates an {@link JCRResource}. 067 * @param node the node backing this {@link AmetysObject} 068 * @param parentPath the parentPath in the Ametys hierarchy 069 * @param factory the DefaultAmetysObjectFactory which created the AmetysObject 070 */ 071 public JCRResource(Node node, String parentPath, F factory) 072 { 073 super(node, parentPath, factory); 074 } 075 076 @Override 077 public void setData(InputStream stream, String mimeType, Date lastModified, UserIdentity author) 078 { 079 Node fileNode = getNode(); 080 081 try 082 { 083 setLastContributor(author); 084 085 Node resourceNode = null; 086 087 if (fileNode.hasNode("jcr:content")) 088 { 089 // Already exists 090 resourceNode = fileNode.getNode("jcr:content"); 091 } 092 else 093 { 094 resourceNode = fileNode.addNode("jcr:content", "nt:resource"); 095 setCreator(author); 096 setCreationDate(new Date()); 097 } 098 099 GregorianCalendar gc = new GregorianCalendar(); 100 gc.setTime(lastModified); 101 resourceNode.setProperty("jcr:lastModified", gc); 102 103 resourceNode.setProperty("jcr:mimeType", mimeType); 104 105 Binary binary = resourceNode.getSession().getValueFactory().createBinary(stream); 106 resourceNode.setProperty("jcr:data", binary); 107 } 108 catch (RepositoryException e) 109 { 110 throw new AmetysRepositoryException("Cannot set data for resource " + this.getName() + " (" + this.getId() + ")", e); 111 } 112 } 113 114 @Override 115 public void setLastModified(Date lastModified) 116 { 117 Node fileNode = getNode(); 118 119 try 120 { 121 Node resourceNode = fileNode.getNode("jcr:content"); 122 123 GregorianCalendar gc = new GregorianCalendar(); 124 gc.setTime(lastModified); 125 resourceNode.setProperty("jcr:lastModified", gc); 126 } 127 catch (RepositoryException e) 128 { 129 throw new AmetysRepositoryException("Cannot set lastmodified for resource " + this.getName() + " (" + this.getId() + ")", e); 130 } 131 } 132 133 @Override 134 public void setKeywords(String keywords) 135 { 136 String[] words = StringUtils.stripAll(StringUtils.split(keywords, ',')); 137 138 String[] trimWords = new String[words.length]; 139 for (int i = 0; i < words.length; i++) 140 { 141 trimWords[i] = words[i].trim(); 142 } 143 144 Node fileNode = getNode(); 145 try 146 { 147 fileNode.setProperty("ametys:keywords", trimWords); 148 } 149 catch (RepositoryException e) 150 { 151 throw new AmetysRepositoryException("Cannot set keywords for resource " + this.getName() + " (" + this.getId() + ")", e); 152 } 153 } 154 155 @Override 156 public void setKeywords(String[] keywords) 157 { 158 Node fileNode = getNode(); 159 try 160 { 161 fileNode.setProperty("ametys:keywords", keywords); 162 } 163 catch (RepositoryException e) 164 { 165 throw new AmetysRepositoryException("Cannot set keywords for resource " + this.getName() + " (" + this.getId() + ")", e); 166 } 167 } 168 169 @Override 170 public void setMimeType(String mimeType) 171 { 172 Node fileNode = getNode(); 173 174 try 175 { 176 Node resourceNode = fileNode.getNode("jcr:content"); 177 178 resourceNode.setProperty("jcr:mimeType", mimeType); 179 } 180 catch (RepositoryException e) 181 { 182 throw new AmetysRepositoryException("Cannot set mimetype for resource " + this.getName() + " (" + this.getId() + ")", e); 183 } 184 } 185 186 @Override 187 public void setCreator(UserIdentity author) 188 { 189 try 190 { 191 Node authorNode = null; 192 if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATOR_NODE_NAME)) 193 { 194 authorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATOR_NODE_NAME); 195 } 196 else 197 { 198 authorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATOR_NODE_NAME, RepositoryConstants.USER_NODETYPE); 199 } 200 authorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", author.getLogin()); 201 authorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", author.getPopulationId()); 202 } 203 catch (RepositoryException e) 204 { 205 throw new AmetysRepositoryException("Cannot set creator for resource " + this.getName() + " (" + this.getId() + ")", e); 206 } 207 } 208 209 @Override 210 public InputStream getInputStream () throws AmetysRepositoryException 211 { 212 Node fileNode = getNode(); 213 try 214 { 215 Node resourceNode = null; 216 217 if (fileNode.hasNode("jcr:content")) 218 { 219 // Already exists 220 resourceNode = fileNode.getNode("jcr:content"); 221 return resourceNode.getProperty("jcr:data").getBinary().getStream(); 222 } 223 return null; 224 } 225 catch (RepositoryException e) 226 { 227 throw new AmetysRepositoryException("Cannot get inputstream for resource " + this.getName() + " (" + this.getId() + ")", e); 228 } 229 } 230 231 @Override 232 public String getMimeType () throws AmetysRepositoryException 233 { 234 Node fileNode = getNode(); 235 try 236 { 237 Node resourceNode = null; 238 239 if (fileNode.hasNode("jcr:content")) 240 { 241 // Already exists 242 resourceNode = fileNode.getNode("jcr:content"); 243 return resourceNode.getProperty("jcr:mimeType").getString(); 244 } 245 246 return null; 247 } 248 catch (RepositoryException e) 249 { 250 throw new AmetysRepositoryException("Cannot get mimetype for resource " + this.getName() + " (" + this.getId() + ")", e); 251 } 252 } 253 254 @Override 255 public Date getLastModified () throws AmetysRepositoryException 256 { 257 Node fileNode = getNode(); 258 try 259 { 260 Node resourceNode = null; 261 262 if (fileNode.hasNode("jcr:content")) 263 { 264 resourceNode = fileNode.getNode("jcr:content"); 265 return resourceNode.getProperty("jcr:lastModified").getDate().getTime(); 266 } 267 268 return null; 269 } 270 catch (RepositoryException e) 271 { 272 throw new AmetysRepositoryException("Cannot get lastmodified for resource " + this.getName() + " (" + this.getId() + ")", e); 273 } 274 } 275 276 @Override 277 public long getLength() throws AmetysRepositoryException 278 { 279 Node fileNode = getNode(); 280 try 281 { 282 Node resourceNode = null; 283 284 if (fileNode.hasNode("jcr:content")) 285 { 286 resourceNode = fileNode.getNode("jcr:content"); 287 return resourceNode.getProperty("jcr:data").getLength(); 288 } 289 290 return 0; 291 } 292 catch (RepositoryException e) 293 { 294 throw new AmetysRepositoryException("Cannot get length for resource " + this.getName() + " (" + this.getId() + ")", e); 295 } 296 } 297 298 @Override 299 public UserIdentity getCreator() throws AmetysRepositoryException 300 { 301 try 302 { 303 Node authorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATOR_NODE_NAME); 304 return new UserIdentity(authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString()); 305 } 306 catch (RepositoryException e) 307 { 308 throw new AmetysRepositoryException("Cannot get creator for resource " + this.getName() + " (" + this.getId() + ")", e); 309 } 310 } 311 312 /** 313 * Get the author from old revision 314 * @param revision The revision 315 * @return The user identity of the author or <code>null</code> if not found 316 * @throws RepositoryException If an error occurred 317 */ 318 public UserIdentity getAuthorFromRevision (String revision) throws RepositoryException 319 { 320 try 321 { 322 switchToRevision(revision); 323 324 VersionHistory history = getVersionHistory(); 325 Node versionNode = history.getVersion(revision); 326 Node frozenNode = versionNode.getNode("jcr:frozenNode"); 327 328 if (frozenNode.hasNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATOR_NODE_NAME)) 329 { 330 Node authorNode = frozenNode.getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATOR_NODE_NAME); 331 return new UserIdentity(authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString()); 332 } 333 return null; 334 } 335 catch (RepositoryException e) 336 { 337 throw new AmetysRepositoryException("Unable to get author from revision: " + revision + " of resource " + this.getName() + " (" + this.getId() + ")", e); 338 } 339 } 340 341 @Override 342 public String[] getKeywords() throws AmetysRepositoryException 343 { 344 Node fileNode = getNode(); 345 try 346 { 347 if (!fileNode.hasProperty("ametys:keywords")) 348 { 349 return new String[0]; 350 } 351 352 Value[] values = fileNode.getProperty("ametys:keywords").getValues(); 353 String[] result = new String[values.length]; 354 355 for (int i = 0; i < values.length; i++) 356 { 357 result[i] = values[i].getString(); 358 } 359 360 return result; 361 } 362 catch (RepositoryException e) 363 { 364 throw new AmetysRepositoryException("Cannot get keywords for resource " + this.getName() + " (" + this.getId() + ")", e); 365 } 366 } 367 368 @Override 369 public String getKeywordsAsString() throws AmetysRepositoryException 370 { 371 Node fileNode = getNode(); 372 try 373 { 374 if (!fileNode.hasProperty("ametys:keywords")) 375 { 376 return StringUtils.EMPTY; 377 } 378 379 StringBuilder sb = new StringBuilder(); 380 Value[] values = fileNode.getProperty("ametys:keywords").getValues(); 381 382 for (Value value : values) 383 { 384 if (sb.length() > 0) 385 { 386 sb.append(", "); 387 } 388 389 sb.append(value.getString()); 390 } 391 392 return sb.toString(); 393 } 394 catch (RepositoryException e) 395 { 396 throw new AmetysRepositoryException("Cannot get keywords for resource " + this.getName() + " (" + this.getId() + ")", e); 397 } 398 } 399 400 @Override 401 protected void restoreFromNode(Node node) throws RepositoryException 402 { 403 super.restoreFromNode(node); 404 405 // First remove node 406 NodeIterator nit = getBaseNode().getNodes("jcr:content"); 407 while (nit.hasNext()) 408 { 409 nit.nextNode().remove(); 410 } 411 412 NodeIterator new_nit = node.getNodes("jcr:content"); 413 while (new_nit.hasNext()) 414 { 415 copyNode(getBaseNode(), new_nit.nextNode()); 416 } 417 } 418 419 @Override 420 public String getResourcePath() throws AmetysRepositoryException 421 { 422 return ((ExplorerNode) getParent()).getExplorerPath() + "/" + getName(); 423 } 424 425 @Override 426 public Thread getComments(boolean createThread) 427 { 428 try 429 { 430 Node node = getNode(); 431 if (!node.hasNode("ametys:comments")) 432 { 433 if (createThread) 434 { 435 Node commentsRootNode = node.addNode("ametys:comments", JCRThreadFactory.THREAD_NODETYPE); 436 437 JCRThread thread = _getFactory().getResolver().resolve(commentsRootNode, false); 438 thread.setTitle(getName()); 439 thread.setDescription(""); 440 thread.setAuthor(getCreator()); 441 thread.setCreationDate(new Date()); 442 443 return thread; 444 } 445 446 return null; 447 } 448 else 449 { 450 Node commentsRootNode = node.getNode("ametys:comments"); 451 return _getFactory().getResolver().resolve(commentsRootNode, false); 452 } 453 } 454 catch (RepositoryException e) 455 { 456 throw new AmetysRepositoryException("Cannot get comments for resource " + this.getName() + " (" + this.getId() + ")", e); 457 } 458 } 459 460 @Override 461 public AmetysObject copyTo(ModifiableTraversableAmetysObject parent, String name) throws AmetysRepositoryException 462 { 463 try 464 { 465 String nodeTypeName = NodeTypeHelper.getNodeTypeName(getNode()); 466 467 JCRResource copiedResource = parent.createChild(name, nodeTypeName); 468 469 copiedResource.setKeywords(getKeywords()); 470 copiedResource.setData(getInputStream(), getMimeType(), getLastModified(), getCreator()); 471 472 parent.saveChanges(); 473 474 return copiedResource; 475 } 476 catch (RepositoryException e) 477 { 478 throw new AmetysRepositoryException("Error copying the collection " + getId() + " into " + parent.getId(), e); 479 } 480 } 481 482 @Override 483 public AmetysObject copyTo(ModifiableTraversableAmetysObject parent, String name, List<String> restrictTo) throws AmetysRepositoryException 484 { 485 return copyTo(parent, name); 486 } 487 488 // Dublin Core metadata. // 489 490 @Override 491 public String getDCTitle() throws AmetysRepositoryException 492 { 493 return DublinCoreHelper.getDCTitle(this, getName()); 494 } 495 496 @Override 497 public void setDCTitle(String title) throws AmetysRepositoryException 498 { 499 DublinCoreHelper.setDCTitle(this, title); 500 } 501 502 @Override 503 public String getDCCreator() throws AmetysRepositoryException 504 { 505 return DublinCoreHelper.getDCCreator(this); 506 } 507 508 @Override 509 public void setDCCreator(String creator) throws AmetysRepositoryException 510 { 511 DublinCoreHelper.setDCCreator(this, creator); 512 } 513 514 @Override 515 public String[] getDCSubject() throws AmetysRepositoryException 516 { 517 return DublinCoreHelper.getDCSubject(this, getKeywords()); 518 } 519 520 @Override 521 public void setDCSubject(String[] subject) throws AmetysRepositoryException 522 { 523 DublinCoreHelper.setDCSubject(this, subject); 524 } 525 526 @Override 527 public String getDCDescription() throws AmetysRepositoryException 528 { 529 return DublinCoreHelper.getDCDescription(this); 530 } 531 532 @Override 533 public void setDCDescription(String description) throws AmetysRepositoryException 534 { 535 DublinCoreHelper.setDCDescription(this, description); 536 } 537 538 @Override 539 public String getDCPublisher() throws AmetysRepositoryException 540 { 541 return DublinCoreHelper.getDCPublisher(this); 542 } 543 544 @Override 545 public void setDCPublisher(String publisher) throws AmetysRepositoryException 546 { 547 DublinCoreHelper.setDCPublisher(this, publisher); 548 } 549 550 @Override 551 public String getDCContributor() throws AmetysRepositoryException 552 { 553 return DublinCoreHelper.getDCContributor(this, UserIdentity.userIdentityToString(getCreator())); 554 } 555 556 @Override 557 public void setDCContributor(String contributor) throws AmetysRepositoryException 558 { 559 DublinCoreHelper.setDCContributor(this, contributor); 560 } 561 562 @Override 563 public Date getDCDate() throws AmetysRepositoryException 564 { 565 return DublinCoreHelper.getDCDate(this, getLastModified()); 566 } 567 568 @Override 569 public void setDCDate(Date date) throws AmetysRepositoryException 570 { 571 DublinCoreHelper.setDCDate(this, date); 572 } 573 574 @Override 575 public String getDCType() throws AmetysRepositoryException 576 { 577 return DublinCoreHelper.getDCType(this, _getDefaultDCType()); 578 } 579 580 private String _getDefaultDCType () 581 { 582 String mimetype = getMimeType(); 583 584 if (mimetype == null) 585 { 586 return DCMITypes.TEXT; 587 } 588 else if (mimetype.startsWith("image")) 589 { 590 return DCMITypes.IMAGE; 591 } 592 else if (mimetype.startsWith("video") || "application/x-shockwave-flash".equals(mimetype)) 593 { 594 return DCMITypes.INTERACTIVERESOURCE; 595 } 596 else if (mimetype.startsWith("audio")) 597 { 598 return DCMITypes.SOUND; 599 } 600 601 return DCMITypes.TEXT; 602 } 603 604 605 @Override 606 public void setDCType(String type) throws AmetysRepositoryException 607 { 608 DublinCoreHelper.setDCType(this, type); 609 } 610 611 @Override 612 public String getDCFormat() throws AmetysRepositoryException 613 { 614 return DublinCoreHelper.getDCFormat(this, getMimeType()); 615 } 616 617 @Override 618 public void setDCFormat(String format) throws AmetysRepositoryException 619 { 620 DublinCoreHelper.setDCFormat(this, format); 621 } 622 623 @Override 624 public String getDCIdentifier() throws AmetysRepositoryException 625 { 626 return DublinCoreHelper.getDCIdentifier(this, getId()); 627 } 628 629 @Override 630 public void setDCIdentifier(String identifier) throws AmetysRepositoryException 631 { 632 DublinCoreHelper.setDCIdentifier(this, identifier); 633 } 634 635 @Override 636 public String getDCSource() throws AmetysRepositoryException 637 { 638 return DublinCoreHelper.getDCSource(this); 639 } 640 641 @Override 642 public void setDCSource(String source) throws AmetysRepositoryException 643 { 644 DublinCoreHelper.setDCSource(this, source); 645 } 646 647 @Override 648 public String getDCLanguage() throws AmetysRepositoryException 649 { 650 return DublinCoreHelper.getDCLanguage(this); 651 } 652 653 @Override 654 public void setDCLanguage(String language) throws AmetysRepositoryException 655 { 656 DublinCoreHelper.setDCLanguage(this, language); 657 } 658 659 @Override 660 public String getDCRelation() throws AmetysRepositoryException 661 { 662 return DublinCoreHelper.getDCRelation(this); 663 } 664 665 @Override 666 public void setDCRelation(String relation) throws AmetysRepositoryException 667 { 668 DublinCoreHelper.setDCRelation(this, relation); 669 } 670 671 @Override 672 public String getDCCoverage() throws AmetysRepositoryException 673 { 674 return DublinCoreHelper.getDCCoverage(this, getDCLanguage()); 675 } 676 677 @Override 678 public void setDCCoverage(String coverage) throws AmetysRepositoryException 679 { 680 DublinCoreHelper.setDCCoverage(this, coverage); 681 } 682 683 @Override 684 public String getDCRights() throws AmetysRepositoryException 685 { 686 return DublinCoreHelper.getDCRights(this); 687 } 688 689 @Override 690 public void setDCRights(String rights) throws AmetysRepositoryException 691 { 692 DublinCoreHelper.setDCRights(this, rights); 693 } 694 695 public Date getCreationDate() throws AmetysRepositoryException 696 { 697 Node fileNode = getNode(); 698 try 699 { 700 if (!fileNode.hasProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATION_DATE)) 701 { 702 return null; 703 } 704 705 return fileNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATION_DATE).getDate().getTime(); 706 } 707 catch (RepositoryException e) 708 { 709 throw new AmetysRepositoryException("Cannot get creation date for resource " + this.getName() + " (" + this.getId() + ")", e); 710 } 711 } 712 713 public void setCreationDate(Date creationDate) 714 { 715 Node fileNode = getNode(); 716 try 717 { 718 Calendar calendar = new GregorianCalendar(); 719 calendar.setTime(creationDate); 720 fileNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":" + CREATION_DATE, calendar); 721 } 722 catch (RepositoryException e) 723 { 724 throw new AmetysRepositoryException("Cannot set create date for resource " + this.getName() + " (" + this.getId() + ")", e); 725 } 726 } 727 728 public UserIdentity getLastContributor() throws AmetysRepositoryException 729 { 730 try 731 { 732 Node authorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CONTRIBUTOR_NODE_NAME); 733 return new UserIdentity(authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login").getString(), authorNode.getProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population").getString()); 734 } 735 catch (RepositoryException e) 736 { 737 throw new AmetysRepositoryException("Cannot get last contributor for resource " + this.getName() + " (" + this.getId() + ")", e); 738 } 739 } 740 741 public void setLastContributor(UserIdentity lastContributor) 742 { 743 try 744 { 745 Node lastContributorNode = null; 746 if (getNode().hasNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CONTRIBUTOR_NODE_NAME)) 747 { 748 lastContributorNode = getNode().getNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CONTRIBUTOR_NODE_NAME); 749 } 750 else 751 { 752 lastContributorNode = getNode().addNode(RepositoryConstants.NAMESPACE_PREFIX + ":" + CONTRIBUTOR_NODE_NAME, RepositoryConstants.USER_NODETYPE); 753 } 754 lastContributorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", lastContributor.getLogin()); 755 lastContributorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", lastContributor.getPopulationId()); 756 } 757 catch (RepositoryException e) 758 { 759 throw new AmetysRepositoryException("Cannot set last contributor for resource " + this.getName() + " (" + this.getId() + ")", e); 760 } 761 } 762 763 public void tag(String tag) throws AmetysRepositoryException 764 { 765 TaggableAmetysObjectHelper.tag(this, tag); 766 } 767 768 public void untag(String tag) throws AmetysRepositoryException 769 { 770 TaggableAmetysObjectHelper.untag(this, tag); 771 } 772 773 public Set<String> getTags() throws AmetysRepositoryException 774 { 775 return TaggableAmetysObjectHelper.getTags(this); 776 } 777}