KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > oracle > toplink > essentials > queryframework > ReadObjectQuery


1 /*
2  * The contents of this file are subject to the terms
3  * of the Common Development and Distribution License
4  * (the "License"). You may not use this file except
5  * in compliance with the License.
6  *
7  * You can obtain a copy of the license at
8  * glassfish/bootstrap/legal/CDDLv1.0.txt or
9  * https://glassfish.dev.java.net/public/CDDLv1.0.html.
10  * See the License for the specific language governing
11  * permissions and limitations under the License.
12  *
13  * When distributing Covered Code, include this CDDL
14  * HEADER in each file and include the License file at
15  * glassfish/bootstrap/legal/CDDLv1.0.txt. If applicable,
16  * add the following below this CDDL HEADER, with the
17  * fields enclosed by brackets "[]" replaced with your
18  * own identifying information: Portions Copyright [yyyy]
19  * [name of copyright owner]
20  */

21 // Copyright (c) 1998, 2006, Oracle. All rights reserved.
22
package oracle.toplink.essentials.queryframework;
23
24 import java.util.*;
25 import oracle.toplink.essentials.internal.helper.*;
26 import oracle.toplink.essentials.exceptions.*;
27 import oracle.toplink.essentials.expressions.*;
28 import oracle.toplink.essentials.internal.descriptors.*;
29 import oracle.toplink.essentials.descriptors.DescriptorQueryManager;
30 import oracle.toplink.essentials.internal.sessions.AbstractRecord;
31 import oracle.toplink.essentials.internal.sessions.UnitOfWorkImpl;
32 import oracle.toplink.essentials.internal.sessions.AbstractSession;
33 import oracle.toplink.essentials.descriptors.ClassDescriptor;
34
35 /**
36  * <p><b>Purpose</b>:
37  * Concrete class for all read queries involving a single object.
38  * <p>
39  * <p><b>Responsibilities</b>:
40  * Return a single object for the query.
41  * Implements the inheritance feature when dealing with abstract descriptors.
42  *
43  * @author Yvon Lavoie
44  * @since TOPLink/Java 1.0
45  */

46 public class ReadObjectQuery extends ObjectLevelReadQuery {
47
48     /** Object that can be used in place of a selection criteria. */
49     protected transient Object JavaDoc selectionObject;
50
51     /** Key that can be used in place of a selection criteria. */
52     protected Vector selectionKey;
53
54     /** Can be used to refresh a specific non-cached instance from the database. */
55     protected boolean shouldLoadResultIntoSelectionObject = false;
56
57     /**
58      * PUBLIC:
59      * Return a new read object query.
60      * A reference class must be specified before execution.
61      * It is better to provide the class and expression builder on construction to esnure a single expression builder is used.
62      * If no selection criteria is specified this will reads the first object found in the database.
63      */

64     public ReadObjectQuery() {
65         super();
66     }
67
68     /**
69      * PUBLIC:
70      * Return a new read object query.
71      * By default, the query has no selection criteria. Executing this query without
72      * selection criteria will always result in a database access to read the first
73      * instance of the specified Class found in the database. This is true no
74      * matter how cache usage is configured and even if an instance of the
75      * specified Class exists in the cache.
76      * Executing a query with selection criteria allows you to avoid a database
77      * access if the selected instance is in the cache. For this reason, you may whish to use a ReadObjectQuery constructor that takes selection criteria, such as: {@link #ReadObjectQuery(Class, Call)}, {@link #ReadObjectQuery(Class, Expression)}, {@link #ReadObjectQuery(Class, ExpressionBuilder)}, {@link #ReadObjectQuery(ExpressionBuilder)}, {@link #ReadObjectQuery(Object)}, or {@link #ReadObjectQuery(Object, QueryByExamplePolicy)}.
78      */

79     public ReadObjectQuery(Class JavaDoc classToRead) {
80         this();
81         setReferenceClass(classToRead);
82     }
83
84     /**
85      * PUBLIC:
86      * Return a new read object query for the class and the selection criteria.
87      */

88     public ReadObjectQuery(Class JavaDoc classToRead, Expression selectionCriteria) {
89         this();
90         setReferenceClass(classToRead);
91         setSelectionCriteria(selectionCriteria);
92     }
93
94     /**
95      * PUBLIC:
96      * Return a new read object query for the class.
97      * The expression builder must be used for all associated expressions used with the query.
98      */

99     public ReadObjectQuery(Class JavaDoc classToRead, ExpressionBuilder builder) {
100         this();
101         this.defaultBuilder = builder;
102         setReferenceClass(classToRead);
103     }
104
105     /**
106      * PUBLIC:
107      * Return a new read object query.
108      * The call represents a database interaction such as SQL, Stored Procedure.
109      */

110     public ReadObjectQuery(Class JavaDoc classToRead, Call call) {
111         this();
112         setReferenceClass(classToRead);
113         setCall(call);
114     }
115
116     /**
117      * PUBLIC:
118      * Return a new read object query.
119      * The call represents a database interaction such as SQL, Stored Procedure.
120      */

121     public ReadObjectQuery(Call call) {
122         this();
123         setCall(call);
124     }
125
126     /**
127      * PUBLIC:
128      * Return a query to read the object with the same primary key as the provided object.
129      * Note: This is not a query by example object, only the primary key will be used for the selection criteria.
130      */

131     public ReadObjectQuery(Object JavaDoc objectToRead) {
132         this();
133         setSelectionObject(objectToRead);
134     }
135
136     /**
137      * PUBLIC:
138      * The expression builder should be provide on creation to ensure only one is used.
139      */

140     public ReadObjectQuery(ExpressionBuilder builder) {
141         this();
142         this.defaultBuilder = builder;
143     }
144
145     /**
146      * PUBLIC:
147      * The cache will be checked only if the query contains exactly the primary key.
148      * Queries can be configured to use the cache at several levels.
149      * Other caching option are available.
150      * @see #setCacheUsage(int)
151      */

152     public void checkCacheByExactPrimaryKey() {
153         setCacheUsage(CheckCacheByExactPrimaryKey);
154     }
155
156     /**
157      * PUBLIC:
158      * This is the default, the cache will be checked only if the query contains the primary key.
159      * Queries can be configured to use the cache at several levels.
160      * Other caching option are available.
161      * @see #setCacheUsage(int)
162      */

163     public void checkCacheByPrimaryKey() {
164         setCacheUsage(CheckCacheByPrimaryKey);
165     }
166
167     /**
168      * PUBLIC:
169      * The cache will be checked completely, then if the object is not found or the query too complex the database will be queried.
170      * Queries can be configured to use the cache at several levels.
171      * Other caching option are available.
172      * @see #setCacheUsage(int)
173      */

174     public void checkCacheThenDatabase() {
175         setCacheUsage(CheckCacheThenDatabase);
176     }
177
178     /**
179      * INTERNAL:
180      * Ensure that the descriptor has been set.
181      */

182     public void checkDescriptor(AbstractSession session) throws QueryException {
183         if (getReferenceClass() == null) {
184             throw QueryException.referenceClassMissing(this);
185         }
186
187         if (getDescriptor() == null) {
188             ClassDescriptor referenceDescriptor;
189             //Bug#3947714 In case getSelectionObject() is proxy
190
if (getSelectionObject() != null && session.getProject().hasProxyIndirection()) {
191                 referenceDescriptor = session.getDescriptor(getSelectionObject());
192             } else {
193                 referenceDescriptor = session.getDescriptor(getReferenceClass());
194             }
195             if (referenceDescriptor == null) {
196                 throw QueryException.descriptorIsMissing(getReferenceClass(), this);
197             }
198             setDescriptor(referenceDescriptor);
199         }
200     }
201
202     /**
203      * INTERNAL:
204      * The cache check is done before the prepare as a hit will not require the work to be done.
205      */

206     protected Object JavaDoc checkEarlyReturnImpl(AbstractSession session, AbstractRecord translationRow) {
207         // Do a cache lookup
208
if (shouldMaintainCache() && (!shouldRefreshIdentityMapResult()) && (!(shouldCheckDescriptorForCacheUsage() && getDescriptor().shouldDisableCacheHits())) && (shouldCheckCache())) {
209             Object JavaDoc cachedObject = getQueryMechanism().checkCacheForObject(translationRow, session);
210
211             // Optimization: If find deleted object by exact primary
212
// key expression or selection object/key just abort.
213
if (cachedObject == InvalidObject.instance) {
214                 return cachedObject;
215             }
216             if (cachedObject != null) {
217                 if (shouldLoadResultIntoSelectionObject()) {
218                     ObjectBuilder builder = getDescriptor().getObjectBuilder();
219                     builder.copyInto(cachedObject, getSelectionObject());
220                     //put this object into the cache. This may cause some loss of identity
221
session.getIdentityMapAccessorInstance().putInIdentityMap(getSelectionObject());
222                     cachedObject = getSelectionObject();
223                 }
224
225                 // check locking. If clone has not been locked, do not early return cached object
226
if (isLockQuery() && (session.isUnitOfWork() && !((UnitOfWorkImpl)session).isPessimisticLocked(cachedObject))) {
227                     return null;
228                 }
229             }
230             if (shouldUseWrapperPolicy()) {
231                 cachedObject = getDescriptor().getObjectBuilder().wrapObject(cachedObject, session);
232             }
233             return cachedObject;
234         } else {
235             return null;
236         }
237     }
238
239     /**
240      * INTERNAL:
241      * Check to see if a custom query should be used for this query.
242      * This is done before the query is copied and prepared/executed.
243      * null means there is none.
244      */

245     protected DatabaseQuery checkForCustomQuery(AbstractSession session, AbstractRecord translationRow) {
246         checkDescriptor(session);
247
248         // check if user defined a custom query in the query manager
249
if (!isUserDefined()) {
250             if (isCallQuery()) {
251                 // this is a hand-coded (custom SQL, SDK etc.) call
252
return null;
253             }
254             DescriptorQueryManager descriptorQueryManager = getDescriptor().getQueryManager();
255
256             // By default all descriptors have a custom ("static") read-object query.
257
// This allows the read-object query and SQL to be prepare once.
258
if (descriptorQueryManager.hasReadObjectQuery()) {
259                 // If the query require special SQL generation or execution do not use the static read object query.
260
// PERF: the read-object query should always be static to ensure no regeneration of SQL.
261
if (getJoinedAttributeManager().hasJoinedAttributeExpressions() || hasPartialAttributeExpressions() || hasAsOfClause() || hasNonDefaultFetchGroup() || (!wasDefaultLockMode()) || (!shouldIgnoreBindAllParameters())) {
262                     return null;
263                 }
264
265                 if ((getSelectionKey() != null) || (getSelectionObject() != null)) {// Must be primary key.
266
return descriptorQueryManager.getReadObjectQuery();
267                 }
268
269                 if (getSelectionCriteria() != null) {
270                     AbstractRecord primaryKeyRow = getDescriptor().getObjectBuilder().extractPrimaryKeyRowFromExpression(getSelectionCriteria(), translationRow, session);
271
272                     // Only execute the query if the selection criteria has the primary key fields set
273
if (primaryKeyRow != null) {
274                         return descriptorQueryManager.getReadObjectQuery();
275                     }
276                 }
277             }
278         }
279
280         return null;
281     }
282
283     /**
284      * INTERNAL:
285      * Conform the result in the UnitOfWork.
286      */

287     protected Object JavaDoc conformResult(Object JavaDoc result, UnitOfWorkImpl unitOfWork, AbstractRecord databaseRow, boolean buildDirectlyFromRows) {
288         // Note that if the object does not conform even though other objects might exist on the database null is returned.
289
// Note that new objects is checked before the read is executed so does not have to be re-checked.
290
// Must unwrap as the built object is always wrapped.
291
// Note the object is unwrapped on the parent which it belongs to, as we
292
// do not want to trigger a registration just yet.
293
Object JavaDoc implementation = null;
294         if (buildDirectlyFromRows) {
295             implementation = result;
296         } else {
297             implementation = getDescriptor().getObjectBuilder().unwrapObject(result, unitOfWork.getParent());
298         }
299
300         Expression selectionCriteriaClone = null;
301         if ((getSelectionCriteria() != null) && (getSelectionKey() == null) && (getSelectionObject() == null)) {
302             selectionCriteriaClone = (Expression)getSelectionCriteria().clone();
303             selectionCriteriaClone.getBuilder().setSession(unitOfWork);
304             selectionCriteriaClone.getBuilder().setQueryClass(getReferenceClass());
305         }
306
307         Object JavaDoc clone = conformIndividualResult(implementation, unitOfWork, databaseRow, selectionCriteriaClone, null, buildDirectlyFromRows);
308         if (clone == null) {
309             return clone;
310         }
311
312         if (shouldUseWrapperPolicy()) {
313             return getDescriptor().getObjectBuilder().wrapObject(clone, unitOfWork);
314         } else {
315             return clone;
316         }
317     }
318
319     /**
320      * PUBLIC:
321      * Do not refesh/load into the selection object, this is the default.
322      * This property allows for the selection object of the query to be refreshed or put into the TopLink cache.
323      * By default on a read or refresh the object in the cache is refreshed and returned or a new object is built from the database,
324      * in some cases such as EJB BMP it is desirable to refresh or load into the object passed into the read object query.
325      * <p>Note: This forces the selection object into the cache a replaces any existing object that may already be there,
326      * this is a strict violation of object identity and other objects can still be refering to the old object.
327      */

328     public void dontLoadResultIntoSelectionObject() {
329         setShouldLoadResultIntoSelectionObject(false);
330     }
331
332     /**
333      * INTERNAL:
334      * Execute the query.
335      * Do a cache lookup and build object from row if required.
336      * @exception DatabaseException - an error has occurred on the database
337      * @return object - the first object found or null if none.
338      */

339     protected Object JavaDoc executeObjectLevelReadQuery() throws DatabaseException {
340         AbstractRecord row = null;
341
342         // If using -m joins, must select all rows.
343
if (getJoinedAttributeManager().isToManyJoin()) {
344             List rows = getQueryMechanism().selectAllRows();
345             if (rows.size() > 0) {
346                 row = (AbstractRecord)rows.get(0);
347             }
348             getJoinedAttributeManager().setDataResults(rows, getSession());
349         } else {
350             row = getQueryMechanism().selectOneRow();
351         }
352         setExecutionTime(System.currentTimeMillis());
353         Object JavaDoc result = null;
354
355         if (getSession().isUnitOfWork()) {
356             result = registerResultInUnitOfWork(row, (UnitOfWorkImpl)getSession(), getTranslationRow(), true);
357         } else {
358             if (row != null) {
359                 result = buildObject(row);
360             }
361         }
362
363         if (shouldIncludeData()) {
364             ComplexQueryResult complexResult = new ComplexQueryResult();
365             complexResult.setResult(result);
366             complexResult.setData(row);
367             return complexResult;
368         }
369
370         return result;
371     }
372
373     /**
374      * PUBLIC:
375      * The primary key can be specified if used instead of an expression or selection object.
376      * If composite the primary must be in the same order as defined in the descriptor.
377      */

378     public Vector getSelectionKey() {
379         return selectionKey;
380
381     }
382
383     /**
384      * PUBLIC:
385      * Return the selection object of the query.
386      * This can be used instead of a where clause expression for single object primary key queries.
387      * The selection object given should have a primary key defined,
388      * this primary key will be used to query the database instance of the same object.
389      * This is a basic form of query by example where only the primary key is required,
390      * it can be used for simple query forms, or testing.
391      */

392     public Object JavaDoc getSelectionObject() {
393         return selectionObject;
394     }
395
396     /**
397      * PUBLIC:
398      * Return if this is a read object query.
399      */

400     public boolean isReadObjectQuery() {
401         return true;
402     }
403
404     /**
405      * PUBLIC:
406      * Allow for the selection object of the query to be refreshed or put into the TopLink cache.
407      * By default on a read or refresh the object in the cache is refreshed and returned or a new object is built from the database,
408      * in some cases such as EJB BMP it is desirable to refresh or load into the object passed into the read object query.
409      * <p>Note: This forces the selection object into the cache a replaces any existing object that may already be there,
410      * this is a strict violation of object identity and other objects can still be refering to the old object.
411      */

412     public void loadResultIntoSelectionObject() {
413         setShouldLoadResultIntoSelectionObject(true);
414     }
415
416     /**
417      * INTERNAL:
418      * Prepare the receiver for execution in a session.
419      */

420     protected void prepare() throws QueryException {
421         super.prepare();
422
423         if ((getSelectionKey() != null) || (getSelectionObject() != null)) {
424             // The expression is set in the prepare as params.
425
setSelectionCriteria(getDescriptor().getObjectBuilder().getPrimaryKeyExpression());
426             // For bug 2989998 the translation row is required to be set at this point.
427
if (!shouldPrepare()) {
428                 if (getSelectionKey() != null) {
429                     // Row must come from the key.
430
setTranslationRow(getDescriptor().getObjectBuilder().buildRowFromPrimaryKeyValues(getSelectionKey(), getSession()));
431                 } else {//(getSelectionObject() != null)
432
setTranslationRow(getDescriptor().getObjectBuilder().buildRowForTranslation(getSelectionObject(), getSession()));
433                 }
434             }
435         }
436
437         // If using -m joining select all rows.
438
if (getJoinedAttributeManager().isToManyJoin()) {
439             getQueryMechanism().prepareSelectAllRows();
440         } else {
441             getQueryMechanism().prepareSelectOneRow();
442         }
443     }
444
445     /**
446      * INTERNAL:
447      * Set the properties needed to be cascaded into the custom query inlucding the translation row.
448      */

449     protected void prepareCustomQuery(DatabaseQuery customQuery) {
450         ReadObjectQuery customReadQuery = (ReadObjectQuery)customQuery;
451         customReadQuery.setShouldRefreshIdentityMapResult(shouldRefreshIdentityMapResult());
452         customReadQuery.setCascadePolicy(getCascadePolicy());
453         customReadQuery.setShouldMaintainCache(shouldMaintainCache());
454         customReadQuery.setShouldUseWrapperPolicy(shouldUseWrapperPolicy());
455         // CR... was missing some values, execution could cause infinite loop.
456
customReadQuery.setQueryId(getQueryId());
457         customReadQuery.setExecutionTime(getExecutionTime());
458         customReadQuery.setShouldLoadResultIntoSelectionObject(shouldLoadResultIntoSelectionObject());
459         AbstractRecord primaryKeyRow;
460         if (getSelectionObject() != null) {
461             // CR#... Must also set the selection object as may be loading into the object (refresh)
462
customReadQuery.setSelectionObject(getSelectionObject());
463             // The translation/primary key row will be set in prepareForExecution.
464
} else if (getSelectionKey() != null) {
465             customReadQuery.setSelectionKey(getSelectionKey());
466         } else {
467             // The primary key row must be used.
468
primaryKeyRow = customQuery.getDescriptor().getObjectBuilder().extractPrimaryKeyRowFromExpression(getSelectionCriteria(), customQuery.getTranslationRow(), customReadQuery.getSession());
469             customReadQuery.setTranslationRow(primaryKeyRow);
470         }
471     }
472
473     /**
474      * INTERNAL:
475      * Prepare the receiver for execution in a session.
476      */

477     public void prepareForExecution() throws QueryException {
478         super.prepareForExecution();
479
480         // For bug 2989998 the translation row now sometimes set earlier in prepare.
481
if (shouldPrepare()) {
482             if (getSelectionKey() != null) {
483                 // Row must come from the key.
484
setTranslationRow(getDescriptor().getObjectBuilder().buildRowFromPrimaryKeyValues(getSelectionKey(), getSession()));
485             } else if (getSelectionObject() != null) {
486                 // The expression is set in the prepare as params.
487
setTranslationRow(getDescriptor().getObjectBuilder().buildRowForTranslation(getSelectionObject(), getSession()));
488             }
489         }
490     }
491
492     /**
493      * INTERNAL:
494      * All objects queried via a UnitOfWork get registered here. If the query
495      * went to the database.
496      * <p>
497      * Involves registering the query result individually and in totality, and
498      * hence refreshing / conforming is done here.
499      * @param result may be collection (read all) or an object (read one),
500      * or even a cursor. If in transaction the shared cache will
501      * be bypassed, meaning the result may not be originals from the parent
502      * but raw database rows.
503      * @param unitOfWork the unitOfWork the result is being registered in.
504      * @param arguments the original arguments/parameters passed to the query
505      * execution. Used by conforming
506      * @param buildDirectlyFromRows If in transaction must construct
507      * a registered result from raw database rows.
508      * @return the final (conformed, refreshed, wrapped) UnitOfWork query result
509      */

510     public Object JavaDoc registerResultInUnitOfWork(Object JavaDoc result, UnitOfWorkImpl unitOfWork, AbstractRecord arguments, boolean buildDirectlyFromRows) {
511         if (result == null) {
512             return null;
513         }
514         if (shouldConformResultsInUnitOfWork() || getDescriptor().shouldAlwaysConformResultsInUnitOfWork()) {
515             return conformResult(result, unitOfWork, arguments, buildDirectlyFromRows);
516         }
517
518         Object JavaDoc clone = registerIndividualResult(result, unitOfWork, buildDirectlyFromRows, null);
519
520         if (shouldUseWrapperPolicy()) {
521             clone = getDescriptor().getObjectBuilder().wrapObject(clone, unitOfWork);
522         }
523         return clone;
524     }
525
526     /**
527      * PUBLIC:
528      * The primary key can be specified if used instead of an expression or selection object.
529      * If composite the primary must be in the same order as defined in the descriptor.
530      */

531     public void setSelectionKey(Vector selectionKey) {
532         this.selectionKey = selectionKey;
533         setIsPrepared(false);
534     }
535
536     /**
537      * PUBLIC:
538      * Used to set the where clause of the query.
539      * This can be used instead of a where clause expression for single object primary key queries.
540      * The selection object given should have a primary key defined,
541      * this primary key will be used to query the database instance of the same object.
542      * This is a basic form of query by example where only the primary key is required,
543      * it can be used for simple query forms, or testing.
544      */

545     public void setSelectionObject(Object JavaDoc selectionObject) {
546         if (selectionObject == null) {
547             throw QueryException.selectionObjectCannotBeNull(this);
548         }
549         setSelectionKey(null);
550         // setIsPrepared(false) triggered by previous.
551
setReferenceClass(selectionObject.getClass());
552         this.selectionObject = selectionObject;
553     }
554
555     /**
556      * PUBLIC:
557      * Allow for the selection object of the query to be refreshed or put into the TopLink cache.
558      * By default on a read or refresh the object in the cache is refreshed and returned or a new object is built from the database,
559      * in some cases such as EJB BMP it is desirable to refresh or load into the object passed into the read object query.
560      * <p>Note: This forces the selection object into the cache a replaces any existing object that may already be there,
561      * this is a strict violation of object identity and other objects can still be refering to the old object.
562      */

563     public void setShouldLoadResultIntoSelectionObject(boolean shouldLoadResultIntoSelectionObject) {
564         this.shouldLoadResultIntoSelectionObject = shouldLoadResultIntoSelectionObject;
565     }
566
567     /**
568      * PUBLIC:
569      * The primary key can be specified if used instead of an expression or selection object.
570      */

571     public void setSingletonSelectionKey(Object JavaDoc selectionKey) {
572         Vector key = new Vector();
573         key.addElement(selectionKey);
574         setSelectionKey(key);
575
576     }
577
578     /**
579      * PUBLIC:
580      * Return if the cache should be checked.
581      */

582     public boolean shouldCheckCache() {
583         return getCacheUsage() != DoNotCheckCache;
584     }
585
586     /**
587      * PUBLIC:
588      * Return if cache should be checked.
589      */

590     public boolean shouldCheckCacheByExactPrimaryKey() {
591         return getCacheUsage() == CheckCacheByExactPrimaryKey;
592     }
593
594     /**
595      * PUBLIC:
596      * Return if cache should be checked.
597      */

598     public boolean shouldCheckCacheByPrimaryKey() {
599         return (getCacheUsage() == CheckCacheByPrimaryKey) || (getCacheUsage() == UseDescriptorSetting);
600     }
601
602     /**
603      * PUBLIC:
604      * Return if cache should be checked.
605      */

606     public boolean shouldCheckCacheThenDatabase() {
607         return getCacheUsage() == CheckCacheThenDatabase;
608     }
609
610     /**
611      * PUBLIC:
612      * return true if the result should be loaded into the passed in selection Object
613      */

614     public boolean shouldLoadResultIntoSelectionObject() {
615         return shouldLoadResultIntoSelectionObject;
616     }
617
618     /**
619      * INTERNAL:
620      * Return if the query has an non-default fetch group defined for itself.
621      */

622     protected boolean hasNonDefaultFetchGroup() {
623         return getDescriptor().hasFetchGroupManager() && ((this.getFetchGroup() != null) || (this.getFetchGroupName() != null) || (!this.shouldUseDefaultFetchGroup()));
624
625     }
626 }
627
Popular Tags