KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > springframework > jmx > export > assembler > AbstractReflectiveMBeanInfoAssembler


1 /*
2  * Copyright 2002-2006 the original author or authors.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  * http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */

16
17 package org.springframework.jmx.export.assembler;
18
19 import java.beans.PropertyDescriptor JavaDoc;
20 import java.lang.reflect.Method JavaDoc;
21 import java.util.ArrayList JavaDoc;
22 import java.util.List JavaDoc;
23
24 import javax.management.Descriptor JavaDoc;
25 import javax.management.JMException JavaDoc;
26 import javax.management.MBeanOperationInfo JavaDoc;
27 import javax.management.MBeanParameterInfo JavaDoc;
28 import javax.management.modelmbean.ModelMBeanAttributeInfo JavaDoc;
29 import javax.management.modelmbean.ModelMBeanOperationInfo JavaDoc;
30
31 import org.springframework.aop.framework.AopProxyUtils;
32 import org.springframework.aop.support.AopUtils;
33 import org.springframework.beans.BeanUtils;
34 import org.springframework.core.JdkVersion;
35 import org.springframework.jmx.support.JmxUtils;
36
37 /**
38  * Builds on the {@link AbstractMBeanInfoAssembler} superclass to
39  * add a basic algorithm for building metadata based on the
40  * reflective metadata of the MBean class.
41  *
42  * <p>The logic for creating MBean metadata from the reflective metadata
43  * is contained in this class, but this class makes no decisions as to
44  * which methods and properties are to be exposed. Instead it gives
45  * subclasses a chance to 'vote' on each property or method through
46  * the <code>includeXXX</code> methods.
47  *
48  * <p>Subclasses are also given the opportunity to populate attribute
49  * and operation metadata with additional descriptors once the metadata
50  * is assembled through the <code>populateXXXDescriptor</code> methods.
51  *
52  * @author Rob Harrop
53  * @author Juergen Hoeller
54  * @since 1.2
55  * @see #includeOperation
56  * @see #includeReadAttribute
57  * @see #includeWriteAttribute
58  * @see #populateAttributeDescriptor
59  * @see #populateOperationDescriptor
60  */

61 public abstract class AbstractReflectiveMBeanInfoAssembler extends AbstractMBeanInfoAssembler {
62
63     /**
64      * Identifies a getter method in a JMX {@link Descriptor}.
65      */

66     protected static final String JavaDoc FIELD_GET_METHOD = "getMethod";
67
68     /**
69      * Identifies a setter method in a JMX {@link Descriptor}.
70      */

71     protected static final String JavaDoc FIELD_SET_METHOD = "setMethod";
72
73     /**
74      * Constant identifier for the role field in a JMX {@link Descriptor}.
75      */

76     protected static final String JavaDoc FIELD_ROLE = "role";
77
78     /**
79      * Constant identifier for the getter role field value in a JMX {@link Descriptor}.
80      */

81     protected static final String JavaDoc ROLE_GETTER = "getter";
82
83     /**
84      * Constant identifier for the setter role field value in a JMX {@link Descriptor}.
85      */

86     protected static final String JavaDoc ROLE_SETTER = "setter";
87
88     /**
89      * Identifies an operation (method) in a JMX {@link Descriptor}.
90      */

91     protected static final String JavaDoc ROLE_OPERATION = "operation";
92
93     /**
94      * Constant identifier for the visibility field in a JMX {@link Descriptor}.
95      */

96     protected static final String JavaDoc FIELD_VISIBILITY = "visibility";
97
98     /**
99      * Lowest visibility, used for operations that correspond to
100      * accessors or mutators for attributes.
101      * @see #FIELD_VISIBILITY
102      */

103     protected static final Integer JavaDoc ATTRIBUTE_OPERATION_VISIBILITY = new Integer JavaDoc(4);
104
105     /**
106      * Constant identifier for the class field in a JMX {@link Descriptor}.
107      */

108     protected static final String JavaDoc FIELD_CLASS = "class";
109     /**
110      * Constant identifier for the log field in a JMX {@link Descriptor}.
111      */

112     protected static final String JavaDoc FIELD_LOG = "log";
113     
114     /**
115      * Constant identifier for the logfile field in a JMX {@link Descriptor}.
116      */

117     protected static final String JavaDoc FIELD_LOG_FILE = "logFile";
118     
119     /**
120      * Constant identifier for the currency time limit field in a JMX {@link Descriptor}.
121      */

122     protected static final String JavaDoc FIELD_CURRENCY_TIME_LIMIT = "currencyTimeLimit";
123
124     /**
125      * Constant identifier for the default field in a JMX {@link Descriptor}.
126      */

127     protected static final String JavaDoc FIELD_DEFAULT = "default";
128
129     /**
130      * Constant identifier for the persistPolicy field in a JMX {@link Descriptor}.
131      */

132     protected static final String JavaDoc FIELD_PERSIST_POLICY = "persistPolicy";
133
134     /**
135      * Constant identifier for the persistPeriod field in a JMX {@link Descriptor}.
136      */

137     protected static final String JavaDoc FIELD_PERSIST_PERIOD = "persistPeriod";
138
139     /**
140      * Constant identifier for the persistLocation field in a JMX {@link Descriptor}.
141      */

142     protected static final String JavaDoc FIELD_PERSIST_LOCATION = "persistLocation";
143
144     /**
145      * Constant identifier for the persistName field in a JMX {@link Descriptor}.
146      */

147     protected static final String JavaDoc FIELD_PERSIST_NAME = "persistName";
148
149
150     /**
151      * Default value for the JMX field "currencyTimeLimit".
152      */

153     private Integer JavaDoc defaultCurrencyTimeLimit;
154
155     /**
156      * Indicates whether or not strict casing is being used for attributes.
157      */

158     private boolean useStrictCasing = true;
159
160     private boolean exposeClassDescriptor = false;
161
162
163     /**
164      * Set the default for the JMX field "currencyTimeLimit".
165      * The default will usually indicate to never cache attribute values.
166      * <p>Default is none, not explicitly setting that field, as recommended by the
167      * JMX 1.2 specification. This should result in "never cache" behavior, always
168      * reading attribute values freshly (which corresponds to a "currencyTimeLimit"
169      * of <code>-1</code> in JMX 1.2).
170      * <p>However, some JMX implementations (that do not follow the JMX 1.2 spec
171      * in that respect) might require an explicit value to be set here to get
172      * "never cache" behavior: for example, JBoss 3.2.x.
173      * <p>Note that the "currencyTimeLimit" value can also be specified on a
174      * managed attribute or operation. The default value will apply if not
175      * overridden with a "currencyTimeLimit" value <code>>= 0</code> there:
176      * a metadata "currencyTimeLimit" value of <code>-1</code> indicates
177      * to use the default; a value of <code>0</code> indicates to "always cache"
178      * and will be translated to <code>Integer.MAX_VALUE</code>; a positive
179      * value indicates the number of cache seconds.
180      * @see org.springframework.jmx.export.metadata.AbstractJmxAttribute#setCurrencyTimeLimit
181      * @see #applyCurrencyTimeLimit(javax.management.Descriptor, int)
182      */

183     public void setDefaultCurrencyTimeLimit(Integer JavaDoc defaultCurrencyTimeLimit) {
184         this.defaultCurrencyTimeLimit = defaultCurrencyTimeLimit;
185     }
186
187     /**
188      * Return default value for the JMX field "currencyTimeLimit", if any.
189      */

190     protected Integer JavaDoc getDefaultCurrencyTimeLimit() {
191         return this.defaultCurrencyTimeLimit;
192     }
193
194     /**
195      * Set whether to use strict casing for attributes. Enabled by default.
196      * <p>When using strict casing, a JavaBean property with a getter such as
197      * <code>getFoo()</code> translates to an attribute called <code>Foo</code>.
198      * With strict casing disabled, <code>getFoo()</code> would translate to just
199      * <code>foo</code>.
200      */

201     public void setUseStrictCasing(boolean useStrictCasing) {
202         this.useStrictCasing = useStrictCasing;
203     }
204
205     /**
206      * Return whether strict casing for attributes is enabled.
207      */

208     protected boolean isUseStrictCasing() {
209         return useStrictCasing;
210     }
211
212     /**
213      * Set whether to expose the JMX descriptor field "class" for managed operations.
214      * Default is "false", letting the JMX implementation determine the actual class
215      * through reflection.
216      * <p>Set this property to <code>true</code> for JMX implementations that
217      * require the "class" field to be specified, for example WebLogic's.
218      * In that case, Spring will expose the target class name there, in case of
219      * a plain bean instance or a CGLIB proxy. When encountering a JDK dynamic
220      * proxy, the <b>first</b> interface implemented by the proxy will be specified.
221      * <p><b>WARNING:</b> Review your proxy definitions when exposing a JDK dynamic
222      * proxy through JMX, in particular with this property turned to <code>true</code>:
223      * the specified interface list should start with your management interface in
224      * this case, with all other interfaces following. In general, consider exposing
225      * your target bean directly or a CGLIB proxy for it instead.
226      * @see #getClassForDescriptor(Object)
227      */

228     public void setExposeClassDescriptor(boolean exposeClassDescriptor) {
229         this.exposeClassDescriptor = exposeClassDescriptor;
230     }
231
232     /**
233      * Return whether to expose the JMX descriptor field "class" for managed operations.
234      */

235     protected boolean isExposeClassDescriptor() {
236         return exposeClassDescriptor;
237     }
238
239
240     /**
241      * Iterate through all properties on the MBean class and gives subclasses
242      * the chance to vote on the inclusion of both the accessor and mutator.
243      * If a particular accessor or mutator is voted for inclusion, the appropriate
244      * metadata is assembled and passed to the subclass for descriptor population.
245      * @param managedBean the bean instance (might be an AOP proxy)
246      * @param beanKey the key associated with the MBean in the beans map
247      * of the <code>MBeanExporter</code>
248      * @return the attribute metadata
249      * @throws JMException in case of errors
250      * @see #populateAttributeDescriptor
251      */

252     protected ModelMBeanAttributeInfo JavaDoc[] getAttributeInfo(Object JavaDoc managedBean, String JavaDoc beanKey) throws JMException JavaDoc {
253         PropertyDescriptor JavaDoc[] props = BeanUtils.getPropertyDescriptors(getClassToExpose(managedBean));
254         List JavaDoc infos = new ArrayList JavaDoc();
255
256         for (int i = 0; i < props.length; i++) {
257             Method JavaDoc getter = props[i].getReadMethod();
258             if (getter != null && getter.getDeclaringClass() == Object JavaDoc.class) {
259                 continue;
260             }
261             if (getter != null && !includeReadAttribute(getter, beanKey)) {
262                 getter = null;
263             }
264
265             Method JavaDoc setter = props[i].getWriteMethod();
266             if (setter != null && !includeWriteAttribute(setter, beanKey)) {
267                 setter = null;
268             }
269
270             if (getter != null || setter != null) {
271                 // If both getter and setter are null, then this does not need exposing.
272
String JavaDoc attrName = JmxUtils.getAttributeName(props[i], isUseStrictCasing());
273                 String JavaDoc description = getAttributeDescription(props[i], beanKey);
274                 ModelMBeanAttributeInfo JavaDoc info = new ModelMBeanAttributeInfo JavaDoc(attrName, description, getter, setter);
275
276                 Descriptor JavaDoc desc = info.getDescriptor();
277                 if (getter != null) {
278                     desc.setField(FIELD_GET_METHOD, getter.getName());
279                 }
280                 if (setter != null) {
281                     desc.setField(FIELD_SET_METHOD, setter.getName());
282                 }
283
284                 populateAttributeDescriptor(desc, getter, setter, beanKey);
285                 info.setDescriptor(desc);
286                 infos.add(info);
287             }
288         }
289
290         return (ModelMBeanAttributeInfo JavaDoc[]) infos.toArray(new ModelMBeanAttributeInfo JavaDoc[infos.size()]);
291     }
292
293     /**
294      * Iterate through all methods on the MBean class and gives subclasses the chance
295      * to vote on their inclusion. If a particular method corresponds to the accessor
296      * or mutator of an attribute that is inclued in the managment interface, then
297      * the corresponding operation is exposed with the &quot;role&quot; descriptor
298      * field set to the appropriate value.
299      * @param managedBean the bean instance (might be an AOP proxy)
300      * @param beanKey the key associated with the MBean in the beans map
301      * of the <code>MBeanExporter</code>
302      * @return the operation metadata
303      * @see #populateOperationDescriptor
304      */

305     protected ModelMBeanOperationInfo JavaDoc[] getOperationInfo(Object JavaDoc managedBean, String JavaDoc beanKey) {
306         Method JavaDoc[] methods = getClassToExpose(managedBean).getMethods();
307         List JavaDoc infos = new ArrayList JavaDoc();
308
309         for (int i = 0; i < methods.length; i++) {
310             Method JavaDoc method = methods[i];
311             if (JdkVersion.isAtLeastJava15() && method.isSynthetic()) {
312                 continue;
313             }
314             if (method.getDeclaringClass().equals(Object JavaDoc.class)) {
315                 continue;
316             }
317
318             ModelMBeanOperationInfo JavaDoc info = null;
319             PropertyDescriptor JavaDoc pd = BeanUtils.findPropertyForMethod(method);
320             if (pd != null) {
321                 if ((method.equals(pd.getReadMethod()) && includeReadAttribute(method, beanKey)) ||
322                         (method.equals(pd.getWriteMethod()) && includeWriteAttribute(method, beanKey))) {
323                     // Attributes need to have their methods exposed as
324
// operations to the JMX server as well.
325
info = createModelMBeanOperationInfo(method, pd.getName(), beanKey);
326                     Descriptor JavaDoc desc = info.getDescriptor();
327                     if (method.equals(pd.getReadMethod())) {
328                         desc.setField(FIELD_ROLE, ROLE_GETTER);
329                     }
330                     else {
331                         desc.setField(FIELD_ROLE, ROLE_SETTER);
332                     }
333                     desc.setField(FIELD_VISIBILITY, ATTRIBUTE_OPERATION_VISIBILITY);
334                     if (isExposeClassDescriptor()) {
335                         desc.setField(FIELD_CLASS, getClassForDescriptor(managedBean).getName());
336                     }
337                     info.setDescriptor(desc);
338                 }
339             }
340             else if (includeOperation(method, beanKey)) {
341                 info = createModelMBeanOperationInfo(method, method.getName(), beanKey);
342                 Descriptor JavaDoc desc = info.getDescriptor();
343                 desc.setField(FIELD_ROLE, ROLE_OPERATION);
344                 if (isExposeClassDescriptor()) {
345                     desc.setField(FIELD_CLASS, getClassForDescriptor(managedBean).getName());
346                 }
347                 populateOperationDescriptor(desc, method, beanKey);
348                 info.setDescriptor(desc);
349             }
350
351             if (info != null) {
352                 infos.add(info);
353             }
354         }
355
356         return (ModelMBeanOperationInfo JavaDoc[]) infos.toArray(new ModelMBeanOperationInfo JavaDoc[infos.size()]);
357     }
358
359     /**
360      * Creates an instance of <code>ModelMBeanOperationInfo</code> for the
361      * given method. Populates the parameter info for the operation.
362      * @param method the <code>Method</code> to create a <code>ModelMBeanOperationInfo</code> for
363      * @param name the name for the operation info
364      * @param beanKey the key associated with the MBean in the beans map
365      * of the <code>MBeanExporter</code>
366      * @return the <code>ModelMBeanOperationInfo</code>
367      */

368     protected ModelMBeanOperationInfo JavaDoc createModelMBeanOperationInfo(Method JavaDoc method, String JavaDoc name, String JavaDoc beanKey) {
369         MBeanParameterInfo JavaDoc[] params = getOperationParameters(method, beanKey);
370         if (params.length == 0) {
371             return new ModelMBeanOperationInfo JavaDoc(getOperationDescription(method, beanKey), method);
372         }
373         else {
374             return new ModelMBeanOperationInfo JavaDoc(name,
375                 getOperationDescription(method, beanKey),
376                 getOperationParameters(method, beanKey),
377                 method.getReturnType().getName(),
378                 MBeanOperationInfo.UNKNOWN);
379         }
380     }
381
382     /**
383      * Return the class to be used for the JMX descriptor field "class".
384      * Only applied when the "exposeClassDescriptor" property is "true".
385      * <p>Default implementation returns the first implemented interface
386      * for a JDK proxy, and the target class else.
387      * @param managedBean the bean instance (might be an AOP proxy)
388      * @return the class to expose in the descriptor field "class"
389      * @see #setExposeClassDescriptor
390      * @see #getClassToExpose(Class)
391      * @see org.springframework.aop.framework.AopProxyUtils#proxiedUserInterfaces(Object)
392      */

393     protected Class JavaDoc getClassForDescriptor(Object JavaDoc managedBean) {
394         if (AopUtils.isJdkDynamicProxy(managedBean)) {
395             return AopProxyUtils.proxiedUserInterfaces(managedBean)[0];
396         }
397         return getClassToExpose(managedBean);
398     }
399
400
401     /**
402      * Allows subclasses to vote on the inclusion of a particular attribute accessor.
403      * @param method the accessor <code>Method</code>
404      * @param beanKey the key associated with the MBean in the beans map
405      * of the <code>MBeanExporter</code>
406      * @return <code>true</code> if the accessor should be included in the management interface,
407      * otherwise <code>false<code>
408      */

409     protected abstract boolean includeReadAttribute(Method JavaDoc method, String JavaDoc beanKey);
410
411     /**
412      * Allows subclasses to vote on the inclusion of a particular attribute mutator.
413      * @param method the mutator <code>Method</code>.
414      * @param beanKey the key associated with the MBean in the beans map
415      * of the <code>MBeanExporter</code>
416      * @return <code>true</code> if the mutator should be included in the management interface,
417      * otherwise <code>false<code>
418      */

419     protected abstract boolean includeWriteAttribute(Method JavaDoc method, String JavaDoc beanKey);
420
421     /**
422      * Allows subclasses to vote on the inclusion of a particular operation.
423      * @param method the operation method
424      * @param beanKey the key associated with the MBean in the beans map
425      * of the <code>MBeanExporter</code>
426      * @return whether the operation should be included in the management interface
427      */

428     protected abstract boolean includeOperation(Method JavaDoc method, String JavaDoc beanKey);
429
430
431     /**
432      * Get the description for a particular attribute.
433      * <p>Default implementation returns a description for the operation
434      * that is the name of corresponding <code>Method</code>.
435      * @param propertyDescriptor the PropertyDescriptor for the attribute
436      * @param beanKey the key associated with the MBean in the beans map
437      * of the <code>MBeanExporter</code>
438      * @return the description for the attribute
439      */

440     protected String JavaDoc getAttributeDescription(PropertyDescriptor JavaDoc propertyDescriptor, String JavaDoc beanKey) {
441         return propertyDescriptor.getDisplayName();
442     }
443
444     /**
445      * Get the description for a particular operation.
446      * <p>Default implementation returns a description for the operation
447      * that is the name of corresponding <code>Method</code>.
448      * @param method the operation method
449      * @param beanKey the key associated with the MBean in the beans map
450      * of the <code>MBeanExporter</code>
451      * @return the description for the operation
452      */

453     protected String JavaDoc getOperationDescription(Method JavaDoc method, String JavaDoc beanKey) {
454         return method.getName();
455     }
456
457     /**
458      * Create parameter info for the given method. Default implementation
459      * returns an empty arry of <code>MBeanParameterInfo</code>.
460      * @param method the <code>Method</code> to get the parameter information for
461      * @param beanKey the key associated with the MBean in the beans map
462      * of the <code>MBeanExporter</code>
463      * @return the <code>MBeanParameterInfo</code> array
464      */

465     protected MBeanParameterInfo JavaDoc[] getOperationParameters(Method JavaDoc method, String JavaDoc beanKey) {
466         return new MBeanParameterInfo JavaDoc[0];
467     }
468
469
470     /**
471      * Allows subclasses to add extra fields to the <code>Descriptor</code> for an
472      * MBean. Default implementation sets the <code>currencyTimeLimit</code> field to
473      * the specified "defaultCurrencyTimeLimit", if any (by default none).
474      * @param descriptor the <code>Descriptor</code> for the MBean resource.
475      * @param managedBean the bean instance (might be an AOP proxy)
476      * @param beanKey the key associated with the MBean in the beans map
477      * of the <code>MBeanExporter</code>
478      * @see #setDefaultCurrencyTimeLimit(Integer)
479      * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor)
480      */

481     protected void populateMBeanDescriptor(Descriptor JavaDoc descriptor, Object JavaDoc managedBean, String JavaDoc beanKey) {
482         applyDefaultCurrencyTimeLimit(descriptor);
483     }
484
485     /**
486      * Allows subclasses to add extra fields to the <code>Descriptor</code> for a particular
487      * attribute. Default implementation sets the <code>currencyTimeLimit</code> field to
488      * the specified "defaultCurrencyTimeLimit", if any (by default none).
489      * @param desc the attribute descriptor
490      * @param getter the accessor method for the attribute
491      * @param setter the mutator method for the attribute
492      * @param beanKey the key associated with the MBean in the beans map
493      * of the <code>MBeanExporter</code>
494      * @see #setDefaultCurrencyTimeLimit(Integer)
495      * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor)
496      */

497     protected void populateAttributeDescriptor(Descriptor JavaDoc desc, Method JavaDoc getter, Method JavaDoc setter, String JavaDoc beanKey) {
498         applyDefaultCurrencyTimeLimit(desc);
499     }
500
501     /**
502      * Allows subclasses to add extra fields to the <code>Descriptor</code> for a particular
503      * operation. Default implementation sets the <code>currencyTimeLimit</code> field to
504      * the specified "defaultCurrencyTimeLimit", if any (by default none).
505      * @param desc the operation descriptor
506      * @param method the method corresponding to the operation
507      * @param beanKey the key associated with the MBean in the beans map
508      * of the <code>MBeanExporter</code>
509      * @see #setDefaultCurrencyTimeLimit(Integer)
510      * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor)
511      */

512     protected void populateOperationDescriptor(Descriptor JavaDoc desc, Method JavaDoc method, String JavaDoc beanKey) {
513         applyDefaultCurrencyTimeLimit(desc);
514     }
515
516     /**
517      * Set the <code>currencyTimeLimit</code> field to the specified
518      * "defaultCurrencyTimeLimit", if any (by default none).
519      * @param desc the JMX attribute or operation descriptor
520      * @see #setDefaultCurrencyTimeLimit(Integer)
521      */

522     protected final void applyDefaultCurrencyTimeLimit(Descriptor JavaDoc desc) {
523         if (getDefaultCurrencyTimeLimit() != null) {
524             desc.setField(FIELD_CURRENCY_TIME_LIMIT, getDefaultCurrencyTimeLimit().toString());
525         }
526     }
527
528     /**
529      * Apply the given JMX "currencyTimeLimit" value to the given descriptor.
530      * <p>Default implementation sets a value <code>>0</code> as-is (as number of cache seconds),
531      * turns a value of <code>0</code> into <code>Integer.MAX_VALUE</code> ("always cache")
532      * and sets the "defaultCurrencyTimeLimit" (if any, indicating "never cache") in case of
533      * a value <code><0</code>. This follows the recommendation in the JMX 1.2 specification.
534      * @param desc the JMX attribute or operation descriptor
535      * @param currencyTimeLimit the "currencyTimeLimit" value to apply
536      * @see #setDefaultCurrencyTimeLimit(Integer)
537      * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor)
538      */

539     protected void applyCurrencyTimeLimit(Descriptor JavaDoc desc, int currencyTimeLimit) {
540         if (currencyTimeLimit > 0) {
541             // number of cache seconds
542
desc.setField(FIELD_CURRENCY_TIME_LIMIT, Integer.toString(currencyTimeLimit));
543         }
544         else if (currencyTimeLimit == 0) {
545             // "always cache"
546
desc.setField(FIELD_CURRENCY_TIME_LIMIT, Integer.toString(Integer.MAX_VALUE));
547         }
548         else {
549             // "never cache"
550
applyDefaultCurrencyTimeLimit(desc);
551         }
552     }
553
554 }
555
Popular Tags