1 21 package oracle.toplink.essentials.mappings; 23 24 import java.security.AccessController ; 25 import java.security.PrivilegedActionException ; 26 import java.util.*; 27 import oracle.toplink.essentials.exceptions.*; 28 import oracle.toplink.essentials.internal.descriptors.*; 29 import oracle.toplink.essentials.internal.security.PrivilegedAccessHelper; 30 import oracle.toplink.essentials.internal.security.PrivilegedClassForName; 31 import oracle.toplink.essentials.internal.sessions.*; 32 import oracle.toplink.essentials.queryframework.*; 33 import oracle.toplink.essentials.sessions.ObjectCopyingPolicy; 34 import oracle.toplink.essentials.descriptors.DescriptorEventManager; 35 import oracle.toplink.essentials.descriptors.DescriptorQueryManager; 36 import oracle.toplink.essentials.internal.sessions.AbstractRecord; 37 import oracle.toplink.essentials.internal.sessions.UnitOfWorkImpl; 38 import oracle.toplink.essentials.internal.sessions.AbstractSession; 39 import oracle.toplink.essentials.descriptors.ClassDescriptor; 40 import oracle.toplink.essentials.internal.queryframework.JoinedAttributeManager; 41 42 51 public abstract class AggregateMapping extends DatabaseMapping { 52 53 54 protected Class referenceClass; 55 protected String referenceClassName; 56 57 58 protected ClassDescriptor referenceDescriptor; 59 60 63 public AggregateMapping() { 64 super(); 65 } 66 67 70 protected DeleteObjectQuery buildAggregateDeleteQuery(DeleteObjectQuery sourceQuery, Object sourceAttributeValue) { 71 DeleteObjectQuery aggregateQuery = new DeleteObjectQuery(); 72 buildAggregateModifyQuery(sourceQuery, aggregateQuery, sourceAttributeValue); 73 return aggregateQuery; 74 } 75 76 79 protected void buildAggregateModifyQuery(ObjectLevelModifyQuery sourceQuery, ObjectLevelModifyQuery aggregateQuery, Object sourceAttributeValue) { 80 if (sourceQuery.getSession().isUnitOfWork()) { 81 Object backupAttributeValue = getAttributeValueFromBackupClone(sourceQuery.getBackupClone()); 82 if (backupAttributeValue == null) { 83 backupAttributeValue = getObjectBuilder(sourceAttributeValue, sourceQuery.getSession()).buildNewInstance(); 84 } 85 aggregateQuery.setBackupClone(backupAttributeValue); 86 } 87 aggregateQuery.setCascadePolicy(sourceQuery.getCascadePolicy()); 88 aggregateQuery.setObject(sourceAttributeValue); 89 aggregateQuery.setTranslationRow(sourceQuery.getTranslationRow()); 90 aggregateQuery.setSession(sourceQuery.getSession()); 91 aggregateQuery.setProperties(sourceQuery.getProperties()); 92 } 93 94 97 protected WriteObjectQuery buildAggregateWriteQuery(WriteObjectQuery sourceQuery, Object sourceAttributeValue) { 98 WriteObjectQuery aggregateQuery = new WriteObjectQuery(); 99 buildAggregateModifyQuery(sourceQuery, aggregateQuery, sourceAttributeValue); 100 return aggregateQuery; 101 } 102 103 107 public void buildBackupClone(Object clone, Object backup, UnitOfWorkImpl unitOfWork) { 108 Object attributeValue = getAttributeValueFromObject(clone); 109 setAttributeValueInObject(backup, buildBackupClonePart(attributeValue, unitOfWork)); 110 } 111 112 116 protected Object buildBackupClonePart(Object attributeValue, UnitOfWorkImpl unitOfWork) { 117 if (attributeValue == null) { 118 return null; 119 } 120 return getObjectBuilder(attributeValue, unitOfWork).buildBackupClone(attributeValue, unitOfWork); 121 } 122 123 127 public void buildClone(Object original, Object clone, UnitOfWorkImpl unitOfWork, JoinedAttributeManager joinedAttributeManager) { 128 Object attributeValue = getAttributeValueFromObject(original); 129 setAttributeValueInObject(clone, buildClonePart(original, attributeValue, unitOfWork)); 130 } 131 132 149 public void buildCloneFromRow(AbstractRecord databaseRow, JoinedAttributeManager joinManager, Object clone, ObjectBuildingQuery sourceQuery, UnitOfWorkImpl unitOfWork, AbstractSession executionSession) { 150 Object cloneAttributeValue = valueFromRow(databaseRow, joinManager, sourceQuery, executionSession); 152 setAttributeValueInObject(clone, cloneAttributeValue); 153 } 154 155 159 protected Object buildClonePart(Object original, Object attributeValue, UnitOfWorkImpl unitOfWork) { 160 if (attributeValue == null) { 161 return null; 162 } 163 if (unitOfWork.isOriginalNewObject(original)) { 164 unitOfWork.addNewAggregate(attributeValue); 165 } 166 167 if (unitOfWork.isClassReadOnly(attributeValue.getClass())) { 169 return attributeValue; 170 } 171 172 ObjectBuilder aggregateObjectBuilder = getObjectBuilder(attributeValue, unitOfWork); 173 174 Object clonedAttributeValue = aggregateObjectBuilder.instantiateWorkingCopyClone(attributeValue, unitOfWork); 176 aggregateObjectBuilder.populateAttributesForClone(attributeValue, clonedAttributeValue, unitOfWork, null); 177 178 return clonedAttributeValue; 179 } 180 181 186 public void buildCopy(Object copy, Object original, ObjectCopyingPolicy policy) { 187 Object attributeValue = getAttributeValueFromObject(original); 188 setAttributeValueInObject(copy, buildCopyOfAttributeValue(attributeValue, policy)); 189 } 190 191 195 protected Object buildCopyOfAttributeValue(Object attributeValue, ObjectCopyingPolicy policy) { 196 if (attributeValue == null) { 197 return null; 198 } 199 return getObjectBuilder(attributeValue, policy.getSession()).copyObject(attributeValue, policy); 200 } 201 202 207 protected Object buildNewMergeInstanceOf(Object sourceAttributeValue, AbstractSession session) { 208 return getObjectBuilder(sourceAttributeValue, session).buildNewInstance(); 209 } 210 211 215 220 224 228 232 protected boolean compareAttributeValues(Object attributeValue1, Object attributeValue2, AbstractSession session) { 233 if ((attributeValue1 == null) && (attributeValue2 == null)) { 234 return true; 235 } 236 if ((attributeValue1 == null) || (attributeValue2 == null)) { 237 return false; 238 } 239 if (attributeValue1.getClass() != attributeValue2.getClass()) { 240 return false; 241 } 242 return getObjectBuilder(attributeValue1, session).compareObjects(attributeValue1, attributeValue2, session); 243 } 244 245 250 public ChangeRecord compareForChange(Object clone, Object backup, ObjectChangeSet owner, AbstractSession session) { 251 Object cloneAttribute = getAttributeValueFromObject(clone); 252 Object backupAttribute = null; 253 254 if (!owner.isNew()) { 255 backupAttribute = getAttributeValueFromObject(backup); 256 if ((cloneAttribute == null) && (backupAttribute == null)) { 257 return null; } 259 if ((cloneAttribute != null) && (backupAttribute != null) && (!cloneAttribute.getClass().equals(backupAttribute.getClass()))) { 260 backupAttribute = null; 261 } 262 } 263 264 AggregateChangeRecord changeRecord = new AggregateChangeRecord(owner); 265 changeRecord.setAttribute(getAttributeName()); 266 changeRecord.setMapping(this); 267 268 if (cloneAttribute == null) { changeRecord.setChangedObject(null); 270 return changeRecord; 271 } 272 273 ObjectBuilder builder = getObjectBuilder(cloneAttribute, session); 274 275 ObjectChangeSet initialChanges = builder.createObjectChangeSet(cloneAttribute, (UnitOfWorkChangeSet)owner.getUOWChangeSet(), (backupAttribute == null), session); 278 ObjectChangeSet changeSet = builder.compareForChange(cloneAttribute, backupAttribute, (UnitOfWorkChangeSet)owner.getUOWChangeSet(), session); 279 if (changeSet == null) { 280 if (initialChanges.isNew()) { 281 changeSet = initialChanges; 285 } else { 286 return null; } 288 } 289 changeRecord.setChangedObject(changeSet); 290 return changeRecord; 291 } 292 293 297 public boolean compareObjects(Object firstObject, Object secondObject, AbstractSession session) { 298 return compareAttributeValues(getAttributeValueFromObject(firstObject), getAttributeValueFromObject(secondObject), session); 299 } 300 301 308 public void convertClassNamesToClasses(ClassLoader classLoader){ 309 Class referenceClass = null; 310 try{ 311 if (PrivilegedAccessHelper.shouldUsePrivilegedAccess()){ 312 try { 313 referenceClass = (Class )AccessController.doPrivileged(new PrivilegedClassForName(getReferenceClassName(), true, classLoader)); 314 } catch (PrivilegedActionException exception) { 315 throw ValidationException.classNotFoundWhileConvertingClassNames(getReferenceClassName(), exception.getException()); 316 } 317 } else { 318 referenceClass = oracle.toplink.essentials.internal.security.PrivilegedAccessHelper.getClassForName(getReferenceClassName(), true, classLoader); 319 } 320 } catch (ClassNotFoundException exc){ 321 throw ValidationException.classNotFoundWhileConvertingClassNames(getReferenceClassName(), exc); 322 } 323 setReferenceClass(referenceClass); 324 }; 325 326 330 protected void executeEvent(int eventCode, ObjectLevelModifyQuery query) { 331 ClassDescriptor referenceDescriptor = getReferenceDescriptor(query.getObject(), query.getSession()); 332 333 if (referenceDescriptor.getEventManager().hasAnyEventListeners()) { 335 referenceDescriptor.getEventManager().executeEvent(new oracle.toplink.essentials.descriptors.DescriptorEvent(eventCode, query)); 336 } 337 } 338 339 344 protected Object getAttributeValueFromBackupClone(Object backupClone) { 345 return getAttributeValueFromObject(backupClone); 346 } 347 348 351 protected ObjectBuilder getObjectBuilderForClass(Class javaClass, AbstractSession session) { 352 return getReferenceDescriptor(javaClass, session).getObjectBuilder(); 353 } 354 355 358 protected ObjectBuilder getObjectBuilder(Object attributeValue, AbstractSession session) { 359 return getReferenceDescriptor(attributeValue, session).getObjectBuilder(); 360 } 361 362 365 protected DescriptorQueryManager getQueryManager(Object attributeValue, AbstractSession session) { 366 return getReferenceDescriptor(attributeValue, session).getQueryManager(); 367 } 368 369 373 public Class getReferenceClass() { 374 return referenceClass; 375 } 376 377 381 public String getReferenceClassName() { 382 if ((referenceClassName == null) && (referenceClass != null)) { 383 referenceClassName = referenceClass.getName(); 384 } 385 return referenceClassName; 386 } 387 388 395 public ClassDescriptor getReferenceDescriptor() { 396 return referenceDescriptor; 397 } 398 399 403 protected ClassDescriptor getReferenceDescriptor(Class theClass, AbstractSession session) { 404 if (getReferenceDescriptor().getJavaClass().equals(theClass)) { 405 return getReferenceDescriptor(); 406 } 407 408 ClassDescriptor subclassDescriptor = session.getDescriptor(theClass); 409 if (subclassDescriptor == null) { 410 throw DescriptorException.noSubClassMatch(theClass, this); 411 } else { 412 return subclassDescriptor; 413 } 414 } 415 416 419 protected ClassDescriptor getReferenceDescriptor(Object attributeValue, AbstractSession session) { 420 if (attributeValue == null) { 421 return getReferenceDescriptor(); 422 } else { 423 return getReferenceDescriptor(attributeValue.getClass(), session); 424 } 425 } 426 427 431 public void initialize(AbstractSession session) throws DescriptorException { 432 super.initialize(session); 433 434 if (getReferenceClass() == null) { 435 throw DescriptorException.referenceClassNotSpecified(this); 436 } 437 438 setReferenceDescriptor(session.getDescriptor(getReferenceClass())); 439 440 ClassDescriptor refDescriptor = this.getReferenceDescriptor(); 441 if (refDescriptor == null) { 442 session.getIntegrityChecker().handleError(DescriptorException.descriptorIsMissing(getReferenceClass().getName(), this)); 443 } 444 if (refDescriptor.isAggregateDescriptor()) { 445 refDescriptor.checkInheritanceTreeAggregateSettings(session, this); 446 } else { 447 session.getIntegrityChecker().handleError(DescriptorException.referenceDescriptorIsNotAggregate(getReferenceClass().getName(), this)); 448 } 449 } 450 451 455 public boolean isAggregateMapping() { 456 return true; 457 } 458 459 463 public void iterate(DescriptorIterator iterator) { 464 iterateOnAttributeValue(iterator, getAttributeValueFromObject(iterator.getVisitedParent())); 465 } 466 467 470 protected void iterateOnAttributeValue(DescriptorIterator iterator, Object attributeValue) { 471 iterator.iterateForAggregateMapping(attributeValue, this, getReferenceDescriptor(attributeValue, iterator.getSession())); 472 } 473 474 477 protected void mergeAttributeValue(Object targetAttributeValue, boolean isTargetUnInitialized, Object sourceAttributeValue, MergeManager mergeManager) { 478 if (mergeManager.getSession().isClassReadOnly(sourceAttributeValue.getClass())) { 480 return; 481 } 482 if (mergeManager.getSession().isClassReadOnly(targetAttributeValue.getClass())) { 483 return; 484 } 485 486 getObjectBuilder(sourceAttributeValue, mergeManager.getSession()).mergeIntoObject(targetAttributeValue, isTargetUnInitialized, sourceAttributeValue, mergeManager); 487 } 488 489 497 public void mergeChangesIntoObject(Object target, ChangeRecord changeRecord, Object source, MergeManager mergeManager) { 498 ObjectChangeSet aggregateChangeSet = (ObjectChangeSet)((AggregateChangeRecord)changeRecord).getChangedObject(); 499 if (aggregateChangeSet == null) { setAttributeValueInObject(target, null); 501 return; 502 } 503 504 Object sourceAggregate = null; 505 if (source != null) { 506 sourceAggregate = getAttributeValueFromObject(source); 507 } 508 ObjectBuilder objectBuilder = getObjectBuilderForClass(aggregateChangeSet.getClassType(mergeManager.getSession()), mergeManager.getSession()); 509 Object targetAggregate = getAttributeValueFromObject(target); 511 if (targetAggregate == null) { 512 targetAggregate = objectBuilder.buildNewInstance(); 513 } else { 514 if ((sourceAggregate != null) && (sourceAggregate.getClass() != targetAggregate.getClass())) { 515 targetAggregate = objectBuilder.buildNewInstance(); 516 } 517 } 518 objectBuilder.mergeChangesIntoObject(targetAggregate, aggregateChangeSet, sourceAggregate, mergeManager); 519 setAttributeValueInObject(target, targetAggregate); 520 } 521 522 527 public void mergeIntoObject(Object target, boolean isTargetUnInitialized, Object source, MergeManager mergeManager) { 528 Object sourceAttributeValue = getAttributeValueFromObject(source); 529 if (sourceAttributeValue == null) { 530 setAttributeValueInObject(target, null); 531 return; 532 } 533 534 Object targetAttributeValue = getAttributeValueFromObject(target); 535 if (targetAttributeValue == null) { 536 targetAttributeValue = buildNewMergeInstanceOf(sourceAttributeValue, mergeManager.getSession()); 539 mergeAttributeValue(targetAttributeValue, true, sourceAttributeValue, mergeManager); 540 this.getDescriptor().getObjectChangePolicy().raiseInternalPropertyChangeEvent(target, getAttributeName(), getAttributeValueFromObject(target), targetAttributeValue); 544 545 } else { 546 mergeAttributeValue(targetAttributeValue, isTargetUnInitialized, sourceAttributeValue, mergeManager); 547 } 548 549 setAttributeValueInObject(target, targetAttributeValue); 551 } 552 553 557 public void postDelete(DeleteObjectQuery query) throws DatabaseException, OptimisticLockException { 558 if (!isReadOnly()) { 559 postDeleteAttributeValue(query, getAttributeValueFromObject(query.getObject())); 560 } 561 } 562 563 567 protected void postDeleteAttributeValue(DeleteObjectQuery query, Object attributeValue) throws DatabaseException, OptimisticLockException { 568 if (attributeValue == null) { 569 return; 570 } 571 DeleteObjectQuery aggregateQuery = buildAggregateDeleteQuery(query, attributeValue); 572 getQueryManager(attributeValue, query.getSession()).postDelete(aggregateQuery); 573 executeEvent(DescriptorEventManager.PostDeleteEvent, aggregateQuery); 574 } 575 576 580 public void postInsert(WriteObjectQuery query) throws DatabaseException, OptimisticLockException { 581 if (!isReadOnly()) { 582 postInsertAttributeValue(query, getAttributeValueFromObject(query.getObject())); 583 } 584 } 585 586 590 protected void postInsertAttributeValue(WriteObjectQuery query, Object attributeValue) throws DatabaseException, OptimisticLockException { 591 if (attributeValue == null) { 592 return; 593 } 594 WriteObjectQuery aggregateQuery = buildAggregateWriteQuery(query, attributeValue); 595 getQueryManager(attributeValue, query.getSession()).postInsert(aggregateQuery); 596 executeEvent(DescriptorEventManager.PostInsertEvent, aggregateQuery); 597 executeEvent(DescriptorEventManager.PostWriteEvent, aggregateQuery); 599 } 600 601 605 public void postUpdate(WriteObjectQuery query) throws DatabaseException, OptimisticLockException { 606 if (!isReadOnly()) { 607 postUpdateAttributeValue(query, getAttributeValueFromObject(query.getObject())); 608 } 609 } 610 611 615 protected void postUpdateAttributeValue(WriteObjectQuery query, Object attributeValue) throws DatabaseException, OptimisticLockException { 616 if (attributeValue == null) { 617 return; 618 } 619 ObjectChangeSet changeSet = null; 620 UnitOfWorkChangeSet uowChangeSet = null; 621 if (query.getSession().isUnitOfWork() && (((UnitOfWorkImpl)query.getSession()).getUnitOfWorkChangeSet() != null)) { 622 uowChangeSet = (UnitOfWorkChangeSet)((UnitOfWorkImpl)query.getSession()).getUnitOfWorkChangeSet(); 623 changeSet = (ObjectChangeSet)uowChangeSet.getObjectChangeSetForClone(attributeValue); 624 } 625 WriteObjectQuery aggregateQuery = buildAggregateWriteQuery(query, attributeValue); 626 aggregateQuery.setObjectChangeSet(changeSet); 627 getQueryManager(attributeValue, query.getSession()).postUpdate(aggregateQuery); 628 executeEvent(DescriptorEventManager.PostUpdateEvent, aggregateQuery); 629 executeEvent(DescriptorEventManager.PostWriteEvent, aggregateQuery); 631 } 632 633 637 public void preDelete(DeleteObjectQuery query) throws DatabaseException, OptimisticLockException { 638 if (!isReadOnly()) { 639 preDeleteAttributeValue(query, getAttributeValueFromObject(query.getObject())); 640 } 641 } 642 643 647 protected void preDeleteAttributeValue(DeleteObjectQuery query, Object attributeValue) throws DatabaseException, OptimisticLockException { 648 if (attributeValue == null) { 649 return; 650 } 651 DeleteObjectQuery aggregateQuery = buildAggregateDeleteQuery(query, attributeValue); 652 executeEvent(DescriptorEventManager.PreDeleteEvent, aggregateQuery); 653 getQueryManager(attributeValue, query.getSession()).preDelete(aggregateQuery); 654 } 655 656 660 public void preInsert(WriteObjectQuery query) throws DatabaseException, OptimisticLockException { 661 if (!isReadOnly()) { 662 preInsertAttributeValue(query, getAttributeValueFromObject(query.getObject())); 663 } 664 } 665 666 670 protected void preInsertAttributeValue(WriteObjectQuery query, Object attributeValue) throws DatabaseException, OptimisticLockException { 671 if (attributeValue == null) { 672 return; 673 } 674 WriteObjectQuery aggregateQuery = buildAggregateWriteQuery(query, attributeValue); 675 getQueryManager(attributeValue, query.getSession()).preInsert(aggregateQuery); 676 } 677 678 682 public void preUpdate(WriteObjectQuery query) throws DatabaseException, OptimisticLockException { 683 if (!isReadOnly()) { 684 preUpdateAttributeValue(query, getAttributeValueFromObject(query.getObject())); 685 } 686 } 687 688 692 protected void preUpdateAttributeValue(WriteObjectQuery query, Object attributeValue) throws DatabaseException, OptimisticLockException { 693 if (attributeValue == null) { 694 return; 695 } 696 WriteObjectQuery aggregateQuery = buildAggregateWriteQuery(query, attributeValue); 697 ObjectChangeSet changeSet = null; 698 UnitOfWorkChangeSet uowChangeSet = null; 699 if (query.getSession().isUnitOfWork() && (((UnitOfWorkImpl)query.getSession()).getUnitOfWorkChangeSet() != null)) { 700 uowChangeSet = (UnitOfWorkChangeSet)((UnitOfWorkImpl)query.getSession()).getUnitOfWorkChangeSet(); 701 changeSet = (ObjectChangeSet)uowChangeSet.getObjectChangeSetForClone(aggregateQuery.getObject()); 702 } 703 704 aggregateQuery.setObjectChangeSet(changeSet); 705 if (changeSet == null) { executeEvent(DescriptorEventManager.PreWriteEvent, aggregateQuery); 708 executeEvent(DescriptorEventManager.PreUpdateEvent, aggregateQuery); 709 } 710 getQueryManager(attributeValue, query.getSession()).preUpdate(aggregateQuery); 711 } 712 713 717 public void setReferenceClass(Class aClass) { 718 referenceClass = aClass; 719 } 720 721 725 public void setReferenceClassName(String aClassName) { 726 referenceClassName = aClassName; 727 } 728 729 734 protected void setReferenceDescriptor(ClassDescriptor aDescriptor) { 735 referenceDescriptor = aDescriptor; 736 } 737 738 745 public void updateChangeRecord(Object sourceClone, Object newValue, Object oldValue, ObjectChangeSet objectChangeSet, UnitOfWorkImpl uow) throws DescriptorException { 746 750 AggregateChangeRecord changeRecord = (AggregateChangeRecord)objectChangeSet.getChangesForAttributeNamed(this.getAttributeName()); 751 if (changeRecord == null){ 752 changeRecord = new AggregateChangeRecord(objectChangeSet); 753 changeRecord.setAttribute(this.getAttributeName()); 754 changeRecord.setMapping(this); 755 objectChangeSet.addChange(changeRecord); 756 } 757 758 if ( sourceClone.getClass().equals(objectChangeSet.getClassType(uow)) ) { 759 ClassDescriptor referenceDescriptor = getReferenceDescriptor(newValue, uow); 761 if ( newValue == null ) { changeRecord.setChangedObject(null); 763 return; 764 }else{ UnitOfWorkChangeSet uowChangeSet = (UnitOfWorkChangeSet)objectChangeSet.getUOWChangeSet(); 766 ObjectChangeSet aggregateChangeSet = (ObjectChangeSet)uowChangeSet.getObjectChangeSetForClone(newValue); 768 if (aggregateChangeSet != null) { 769 aggregateChangeSet.clear(); } 771 changeRecord.setChangedObject(referenceDescriptor.getObjectChangePolicy().createObjectChangeSetThroughComparison(newValue,oldValue, uowChangeSet, (oldValue == null), uow, referenceDescriptor)); 773 referenceDescriptor.getObjectChangePolicy().setChangeSetOnListener((ObjectChangeSet)changeRecord.getChangedObject(), newValue); 774 775 } 776 } else { 777 changeRecord.setChangedObject(referenceDescriptor.getObjectChangePolicy().createObjectChangeSetThroughComparison(sourceClone, null, (UnitOfWorkChangeSet)objectChangeSet.getUOWChangeSet(), true, uow, referenceDescriptor)); 779 } 780 } 781 782 786 public boolean verifyDelete(Object object, AbstractSession session) throws DatabaseException { 787 return verifyDeleteOfAttributeValue(getAttributeValueFromObject(object), session); 788 } 789 790 794 protected boolean verifyDeleteOfAttributeValue(Object attributeValue, AbstractSession session) throws DatabaseException { 795 if (attributeValue == null) { 796 return true; 797 } 798 for (Enumeration mappings = getReferenceDescriptor(attributeValue, session).getMappings().elements(); 799 mappings.hasMoreElements();) { 800 DatabaseMapping mapping = (DatabaseMapping)mappings.nextElement(); 801 if (!mapping.verifyDelete(attributeValue, session)) { 802 return false; 803 } 804 } 805 return true; 806 } 807 } 808 | Popular Tags |