001/*
002 * Copyright 2002-2018 the original author or authors.
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 *      https://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 */
016
017package org.springframework.jmx.export.assembler;
018
019import java.beans.PropertyDescriptor;
020import java.lang.reflect.Method;
021import java.util.ArrayList;
022import java.util.List;
023import javax.management.Descriptor;
024import javax.management.JMException;
025import javax.management.MBeanOperationInfo;
026import javax.management.MBeanParameterInfo;
027import javax.management.modelmbean.ModelMBeanAttributeInfo;
028import javax.management.modelmbean.ModelMBeanOperationInfo;
029
030import org.springframework.aop.framework.AopProxyUtils;
031import org.springframework.aop.support.AopUtils;
032import org.springframework.beans.BeanUtils;
033import org.springframework.core.DefaultParameterNameDiscoverer;
034import org.springframework.core.ParameterNameDiscoverer;
035import org.springframework.jmx.support.JmxUtils;
036
037/**
038 * Builds on the {@link AbstractMBeanInfoAssembler} superclass to
039 * add a basic algorithm for building metadata based on the
040 * reflective metadata of the MBean class.
041 *
042 * <p>The logic for creating MBean metadata from the reflective metadata
043 * is contained in this class, but this class makes no decisions as to
044 * which methods and properties are to be exposed. Instead it gives
045 * subclasses a chance to 'vote' on each property or method through
046 * the {@code includeXXX} methods.
047 *
048 * <p>Subclasses are also given the opportunity to populate attribute
049 * and operation metadata with additional descriptors once the metadata
050 * is assembled through the {@code populateXXXDescriptor} methods.
051 *
052 * @author Rob Harrop
053 * @author Juergen Hoeller
054 * @author David Boden
055 * @since 1.2
056 * @see #includeOperation
057 * @see #includeReadAttribute
058 * @see #includeWriteAttribute
059 * @see #populateAttributeDescriptor
060 * @see #populateOperationDescriptor
061 */
062public abstract class AbstractReflectiveMBeanInfoAssembler extends AbstractMBeanInfoAssembler {
063
064        /**
065         * Identifies a getter method in a JMX {@link Descriptor}.
066         */
067        protected static final String FIELD_GET_METHOD = "getMethod";
068
069        /**
070         * Identifies a setter method in a JMX {@link Descriptor}.
071         */
072        protected static final String FIELD_SET_METHOD = "setMethod";
073
074        /**
075         * Constant identifier for the role field in a JMX {@link Descriptor}.
076         */
077        protected static final String FIELD_ROLE = "role";
078
079        /**
080         * Constant identifier for the getter role field value in a JMX {@link Descriptor}.
081         */
082        protected static final String ROLE_GETTER = "getter";
083
084        /**
085         * Constant identifier for the setter role field value in a JMX {@link Descriptor}.
086         */
087        protected static final String ROLE_SETTER = "setter";
088
089        /**
090         * Identifies an operation (method) in a JMX {@link Descriptor}.
091         */
092        protected static final String ROLE_OPERATION = "operation";
093
094        /**
095         * Constant identifier for the visibility field in a JMX {@link Descriptor}.
096         */
097        protected static final String FIELD_VISIBILITY = "visibility";
098
099        /**
100         * Lowest visibility, used for operations that correspond to
101         * accessors or mutators for attributes.
102         * @see #FIELD_VISIBILITY
103         */
104        protected static final int ATTRIBUTE_OPERATION_VISIBILITY = 4;
105
106        /**
107         * Constant identifier for the class field in a JMX {@link Descriptor}.
108         */
109        protected static final String FIELD_CLASS = "class";
110        /**
111         * Constant identifier for the log field in a JMX {@link Descriptor}.
112         */
113        protected static final String FIELD_LOG = "log";
114
115        /**
116         * Constant identifier for the logfile field in a JMX {@link Descriptor}.
117         */
118        protected static final String FIELD_LOG_FILE = "logFile";
119
120        /**
121         * Constant identifier for the currency time limit field in a JMX {@link Descriptor}.
122         */
123        protected static final String FIELD_CURRENCY_TIME_LIMIT = "currencyTimeLimit";
124
125        /**
126         * Constant identifier for the default field in a JMX {@link Descriptor}.
127         */
128        protected static final String FIELD_DEFAULT = "default";
129
130        /**
131         * Constant identifier for the persistPolicy field in a JMX {@link Descriptor}.
132         */
133        protected static final String FIELD_PERSIST_POLICY = "persistPolicy";
134
135        /**
136         * Constant identifier for the persistPeriod field in a JMX {@link Descriptor}.
137         */
138        protected static final String FIELD_PERSIST_PERIOD = "persistPeriod";
139
140        /**
141         * Constant identifier for the persistLocation field in a JMX {@link Descriptor}.
142         */
143        protected static final String FIELD_PERSIST_LOCATION = "persistLocation";
144
145        /**
146         * Constant identifier for the persistName field in a JMX {@link Descriptor}.
147         */
148        protected static final String FIELD_PERSIST_NAME = "persistName";
149
150        /**
151         * Constant identifier for the displayName field in a JMX {@link Descriptor}.
152         */
153        protected static final String FIELD_DISPLAY_NAME = "displayName";
154
155        /**
156         * Constant identifier for the units field in a JMX {@link Descriptor}.
157         */
158        protected static final String FIELD_UNITS = "units";
159
160        /**
161         * Constant identifier for the metricType field in a JMX {@link Descriptor}.
162         */
163        protected static final String FIELD_METRIC_TYPE = "metricType";
164
165        /**
166         * Constant identifier for the custom metricCategory field in a JMX {@link Descriptor}.
167         */
168        protected static final String FIELD_METRIC_CATEGORY = "metricCategory";
169
170
171        /**
172         * Default value for the JMX field "currencyTimeLimit".
173         */
174        private Integer defaultCurrencyTimeLimit;
175
176        /**
177         * Indicates whether or not strict casing is being used for attributes.
178         */
179        private boolean useStrictCasing = true;
180
181        private boolean exposeClassDescriptor = false;
182
183        private ParameterNameDiscoverer parameterNameDiscoverer = new DefaultParameterNameDiscoverer();
184
185
186        /**
187         * Set the default for the JMX field "currencyTimeLimit".
188         * The default will usually indicate to never cache attribute values.
189         * <p>Default is none, not explicitly setting that field, as recommended by the
190         * JMX 1.2 specification. This should result in "never cache" behavior, always
191         * reading attribute values freshly (which corresponds to a "currencyTimeLimit"
192         * of {@code -1} in JMX 1.2).
193         * <p>However, some JMX implementations (that do not follow the JMX 1.2 spec
194         * in that respect) might require an explicit value to be set here to get
195         * "never cache" behavior: for example, JBoss 3.2.x.
196         * <p>Note that the "currencyTimeLimit" value can also be specified on a
197         * managed attribute or operation. The default value will apply if not
198         * overridden with a "currencyTimeLimit" value {@code >= 0} there:
199         * a metadata "currencyTimeLimit" value of {@code -1} indicates
200         * to use the default; a value of {@code 0} indicates to "always cache"
201         * and will be translated to {@code Integer.MAX_VALUE}; a positive
202         * value indicates the number of cache seconds.
203         * @see org.springframework.jmx.export.metadata.AbstractJmxAttribute#setCurrencyTimeLimit
204         * @see #applyCurrencyTimeLimit(javax.management.Descriptor, int)
205         */
206        public void setDefaultCurrencyTimeLimit(Integer defaultCurrencyTimeLimit) {
207                this.defaultCurrencyTimeLimit = defaultCurrencyTimeLimit;
208        }
209
210        /**
211         * Return default value for the JMX field "currencyTimeLimit", if any.
212         */
213        protected Integer getDefaultCurrencyTimeLimit() {
214                return this.defaultCurrencyTimeLimit;
215        }
216
217        /**
218         * Set whether to use strict casing for attributes. Enabled by default.
219         * <p>When using strict casing, a JavaBean property with a getter such as
220         * {@code getFoo()} translates to an attribute called {@code Foo}.
221         * With strict casing disabled, {@code getFoo()} would translate to just
222         * {@code foo}.
223         */
224        public void setUseStrictCasing(boolean useStrictCasing) {
225                this.useStrictCasing = useStrictCasing;
226        }
227
228        /**
229         * Return whether strict casing for attributes is enabled.
230         */
231        protected boolean isUseStrictCasing() {
232                return this.useStrictCasing;
233        }
234
235        /**
236         * Set whether to expose the JMX descriptor field "class" for managed operations.
237         * Default is "false", letting the JMX implementation determine the actual class
238         * through reflection.
239         * <p>Set this property to {@code true} for JMX implementations that
240         * require the "class" field to be specified, for example WebLogic's.
241         * In that case, Spring will expose the target class name there, in case of
242         * a plain bean instance or a CGLIB proxy. When encountering a JDK dynamic
243         * proxy, the <b>first</b> interface implemented by the proxy will be specified.
244         * <p><b>WARNING:</b> Review your proxy definitions when exposing a JDK dynamic
245         * proxy through JMX, in particular with this property turned to {@code true}:
246         * the specified interface list should start with your management interface in
247         * this case, with all other interfaces following. In general, consider exposing
248         * your target bean directly or a CGLIB proxy for it instead.
249         * @see #getClassForDescriptor(Object)
250         */
251        public void setExposeClassDescriptor(boolean exposeClassDescriptor) {
252                this.exposeClassDescriptor = exposeClassDescriptor;
253        }
254
255        /**
256         * Return whether to expose the JMX descriptor field "class" for managed operations.
257         */
258        protected boolean isExposeClassDescriptor() {
259                return this.exposeClassDescriptor;
260        }
261
262        /**
263         * Set the ParameterNameDiscoverer to use for resolving method parameter
264         * names if needed (e.g. for parameter names of MBean operation methods).
265         * <p>Default is a {@link DefaultParameterNameDiscoverer}.
266         */
267        public void setParameterNameDiscoverer(ParameterNameDiscoverer parameterNameDiscoverer) {
268                this.parameterNameDiscoverer = parameterNameDiscoverer;
269        }
270
271        /**
272         * Return the ParameterNameDiscoverer to use for resolving method parameter
273         * names if needed (may be {@code null} in order to skip parameter detection).
274         */
275        protected ParameterNameDiscoverer getParameterNameDiscoverer() {
276                return this.parameterNameDiscoverer;
277        }
278
279
280        /**
281         * Iterate through all properties on the MBean class and gives subclasses
282         * the chance to vote on the inclusion of both the accessor and mutator.
283         * If a particular accessor or mutator is voted for inclusion, the appropriate
284         * metadata is assembled and passed to the subclass for descriptor population.
285         * @param managedBean the bean instance (might be an AOP proxy)
286         * @param beanKey the key associated with the MBean in the beans map
287         * of the {@code MBeanExporter}
288         * @return the attribute metadata
289         * @throws JMException in case of errors
290         * @see #populateAttributeDescriptor
291         */
292        @Override
293        protected ModelMBeanAttributeInfo[] getAttributeInfo(Object managedBean, String beanKey) throws JMException {
294                PropertyDescriptor[] props = BeanUtils.getPropertyDescriptors(getClassToExpose(managedBean));
295                List<ModelMBeanAttributeInfo> infos = new ArrayList<ModelMBeanAttributeInfo>();
296
297                for (PropertyDescriptor prop : props) {
298                        Method getter = prop.getReadMethod();
299                        if (getter != null && getter.getDeclaringClass() == Object.class) {
300                                continue;
301                        }
302                        if (getter != null && !includeReadAttribute(getter, beanKey)) {
303                                getter = null;
304                        }
305
306                        Method setter = prop.getWriteMethod();
307                        if (setter != null && !includeWriteAttribute(setter, beanKey)) {
308                                setter = null;
309                        }
310
311                        if (getter != null || setter != null) {
312                                // If both getter and setter are null, then this does not need exposing.
313                                String attrName = JmxUtils.getAttributeName(prop, isUseStrictCasing());
314                                String description = getAttributeDescription(prop, beanKey);
315                                ModelMBeanAttributeInfo info = new ModelMBeanAttributeInfo(attrName, description, getter, setter);
316
317                                Descriptor desc = info.getDescriptor();
318                                if (getter != null) {
319                                        desc.setField(FIELD_GET_METHOD, getter.getName());
320                                }
321                                if (setter != null) {
322                                        desc.setField(FIELD_SET_METHOD, setter.getName());
323                                }
324
325                                populateAttributeDescriptor(desc, getter, setter, beanKey);
326                                info.setDescriptor(desc);
327                                infos.add(info);
328                        }
329                }
330
331                return infos.toArray(new ModelMBeanAttributeInfo[infos.size()]);
332        }
333
334        /**
335         * Iterate through all methods on the MBean class and gives subclasses the chance
336         * to vote on their inclusion. If a particular method corresponds to the accessor
337         * or mutator of an attribute that is included in the management interface, then
338         * the corresponding operation is exposed with the &quot;role&quot; descriptor
339         * field set to the appropriate value.
340         * @param managedBean the bean instance (might be an AOP proxy)
341         * @param beanKey the key associated with the MBean in the beans map
342         * of the {@code MBeanExporter}
343         * @return the operation metadata
344         * @see #populateOperationDescriptor
345         */
346        @Override
347        protected ModelMBeanOperationInfo[] getOperationInfo(Object managedBean, String beanKey) {
348                Method[] methods = getClassToExpose(managedBean).getMethods();
349                List<ModelMBeanOperationInfo> infos = new ArrayList<ModelMBeanOperationInfo>();
350
351                for (Method method : methods) {
352                        if (method.isSynthetic()) {
353                                continue;
354                        }
355                        if (Object.class == method.getDeclaringClass()) {
356                                continue;
357                        }
358
359                        ModelMBeanOperationInfo info = null;
360                        PropertyDescriptor pd = BeanUtils.findPropertyForMethod(method);
361                        if (pd != null && ((method.equals(pd.getReadMethod()) && includeReadAttribute(method, beanKey)) ||
362                                                (method.equals(pd.getWriteMethod()) && includeWriteAttribute(method, beanKey)))) {
363                                // Attributes need to have their methods exposed as
364                                // operations to the JMX server as well.
365                                info = createModelMBeanOperationInfo(method, pd.getName(), beanKey);
366                                Descriptor desc = info.getDescriptor();
367                                if (method.equals(pd.getReadMethod())) {
368                                        desc.setField(FIELD_ROLE, ROLE_GETTER);
369                                }
370                                else {
371                                        desc.setField(FIELD_ROLE, ROLE_SETTER);
372                                }
373                                desc.setField(FIELD_VISIBILITY, ATTRIBUTE_OPERATION_VISIBILITY);
374                                if (isExposeClassDescriptor()) {
375                                        desc.setField(FIELD_CLASS, getClassForDescriptor(managedBean).getName());
376                                }
377                                info.setDescriptor(desc);
378                        }
379
380                        // allow getters and setters to be marked as operations directly
381                        if (info == null && includeOperation(method, beanKey)) {
382                                info = createModelMBeanOperationInfo(method, method.getName(), beanKey);
383                                Descriptor desc = info.getDescriptor();
384                                desc.setField(FIELD_ROLE, ROLE_OPERATION);
385                                if (isExposeClassDescriptor()) {
386                                        desc.setField(FIELD_CLASS, getClassForDescriptor(managedBean).getName());
387                                }
388                                populateOperationDescriptor(desc, method, beanKey);
389                                info.setDescriptor(desc);
390                        }
391
392                        if (info != null) {
393                                infos.add(info);
394                        }
395                }
396
397                return infos.toArray(new ModelMBeanOperationInfo[infos.size()]);
398        }
399
400        /**
401         * Creates an instance of {@code ModelMBeanOperationInfo} for the
402         * given method. Populates the parameter info for the operation.
403         * @param method the {@code Method} to create a {@code ModelMBeanOperationInfo} for
404         * @param name the logical name for the operation (method name or property name);
405         * not used by the default implementation but possibly by subclasses
406         * @param beanKey the key associated with the MBean in the beans map
407         * of the {@code MBeanExporter}
408         * @return the {@code ModelMBeanOperationInfo}
409         */
410        protected ModelMBeanOperationInfo createModelMBeanOperationInfo(Method method, String name, String beanKey) {
411                MBeanParameterInfo[] params = getOperationParameters(method, beanKey);
412                if (params.length == 0) {
413                        return new ModelMBeanOperationInfo(getOperationDescription(method, beanKey), method);
414                }
415                else {
416                        return new ModelMBeanOperationInfo(method.getName(),
417                                getOperationDescription(method, beanKey),
418                                getOperationParameters(method, beanKey),
419                                method.getReturnType().getName(),
420                                MBeanOperationInfo.UNKNOWN);
421                }
422        }
423
424        /**
425         * Return the class to be used for the JMX descriptor field "class".
426         * Only applied when the "exposeClassDescriptor" property is "true".
427         * <p>The default implementation returns the first implemented interface
428         * for a JDK proxy, and the target class else.
429         * @param managedBean the bean instance (might be an AOP proxy)
430         * @return the class to expose in the descriptor field "class"
431         * @see #setExposeClassDescriptor
432         * @see #getClassToExpose(Class)
433         * @see org.springframework.aop.framework.AopProxyUtils#proxiedUserInterfaces(Object)
434         */
435        protected Class<?> getClassForDescriptor(Object managedBean) {
436                if (AopUtils.isJdkDynamicProxy(managedBean)) {
437                        return AopProxyUtils.proxiedUserInterfaces(managedBean)[0];
438                }
439                return getClassToExpose(managedBean);
440        }
441
442
443        /**
444         * Allows subclasses to vote on the inclusion of a particular attribute accessor.
445         * @param method the accessor {@code Method}
446         * @param beanKey the key associated with the MBean in the beans map
447         * of the {@code MBeanExporter}
448         * @return {@code true} if the accessor should be included in the management interface,
449         * otherwise {@code false}
450         */
451        protected abstract boolean includeReadAttribute(Method method, String beanKey);
452
453        /**
454         * Allows subclasses to vote on the inclusion of a particular attribute mutator.
455         * @param method the mutator {@code Method}.
456         * @param beanKey the key associated with the MBean in the beans map
457         * of the {@code MBeanExporter}
458         * @return {@code true} if the mutator should be included in the management interface,
459         * otherwise {@code false}
460         */
461        protected abstract boolean includeWriteAttribute(Method method, String beanKey);
462
463        /**
464         * Allows subclasses to vote on the inclusion of a particular operation.
465         * @param method the operation method
466         * @param beanKey the key associated with the MBean in the beans map
467         * of the {@code MBeanExporter}
468         * @return whether the operation should be included in the management interface
469         */
470        protected abstract boolean includeOperation(Method method, String beanKey);
471
472        /**
473         * Get the description for a particular attribute.
474         * <p>The default implementation returns a description for the operation
475         * that is the name of corresponding {@code Method}.
476         * @param propertyDescriptor the PropertyDescriptor for the attribute
477         * @param beanKey the key associated with the MBean in the beans map
478         * of the {@code MBeanExporter}
479         * @return the description for the attribute
480         */
481        protected String getAttributeDescription(PropertyDescriptor propertyDescriptor, String beanKey) {
482                return propertyDescriptor.getDisplayName();
483        }
484
485        /**
486         * Get the description for a particular operation.
487         * <p>The default implementation returns a description for the operation
488         * that is the name of corresponding {@code Method}.
489         * @param method the operation method
490         * @param beanKey the key associated with the MBean in the beans map
491         * of the {@code MBeanExporter}
492         * @return the description for the operation
493         */
494        protected String getOperationDescription(Method method, String beanKey) {
495                return method.getName();
496        }
497
498        /**
499         * Create parameter info for the given method.
500         * <p>The default implementation returns an empty array of {@code MBeanParameterInfo}.
501         * @param method the {@code Method} to get the parameter information for
502         * @param beanKey the key associated with the MBean in the beans map
503         * of the {@code MBeanExporter}
504         * @return the {@code MBeanParameterInfo} array
505         */
506        protected MBeanParameterInfo[] getOperationParameters(Method method, String beanKey) {
507                ParameterNameDiscoverer paramNameDiscoverer = getParameterNameDiscoverer();
508                String[] paramNames = (paramNameDiscoverer != null ? paramNameDiscoverer.getParameterNames(method) : null);
509                if (paramNames == null) {
510                        return new MBeanParameterInfo[0];
511                }
512
513                MBeanParameterInfo[] info = new MBeanParameterInfo[paramNames.length];
514                Class<?>[] typeParameters = method.getParameterTypes();
515                for (int i = 0; i < info.length; i++) {
516                        info[i] = new MBeanParameterInfo(paramNames[i], typeParameters[i].getName(), paramNames[i]);
517                }
518
519                return info;
520        }
521
522        /**
523         * Allows subclasses to add extra fields to the {@code Descriptor} for an MBean.
524         * <p>The default implementation sets the {@code currencyTimeLimit} field to
525         * the specified "defaultCurrencyTimeLimit", if any (by default none).
526         * @param descriptor the {@code Descriptor} for the MBean resource.
527         * @param managedBean the bean instance (might be an AOP proxy)
528         * @param beanKey the key associated with the MBean in the beans map
529         * of the {@code MBeanExporter}
530         * @see #setDefaultCurrencyTimeLimit(Integer)
531         * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor)
532         */
533        @Override
534        protected void populateMBeanDescriptor(Descriptor descriptor, Object managedBean, String beanKey) {
535                applyDefaultCurrencyTimeLimit(descriptor);
536        }
537
538        /**
539         * Allows subclasses to add extra fields to the {@code Descriptor} for a
540         * particular attribute.
541         * <p>The default implementation sets the {@code currencyTimeLimit} field to
542         * the specified "defaultCurrencyTimeLimit", if any (by default none).
543         * @param desc the attribute descriptor
544         * @param getter the accessor method for the attribute
545         * @param setter the mutator method for the attribute
546         * @param beanKey the key associated with the MBean in the beans map
547         * of the {@code MBeanExporter}
548         * @see #setDefaultCurrencyTimeLimit(Integer)
549         * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor)
550         */
551        protected void populateAttributeDescriptor(Descriptor desc, Method getter, Method setter, String beanKey) {
552                applyDefaultCurrencyTimeLimit(desc);
553        }
554
555        /**
556         * Allows subclasses to add extra fields to the {@code Descriptor} for a
557         * particular operation.
558         * <p>The default implementation sets the {@code currencyTimeLimit} field to
559         * the specified "defaultCurrencyTimeLimit", if any (by default none).
560         * @param desc the operation descriptor
561         * @param method the method corresponding to the operation
562         * @param beanKey the key associated with the MBean in the beans map
563         * of the {@code MBeanExporter}
564         * @see #setDefaultCurrencyTimeLimit(Integer)
565         * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor)
566         */
567        protected void populateOperationDescriptor(Descriptor desc, Method method, String beanKey) {
568                applyDefaultCurrencyTimeLimit(desc);
569        }
570
571        /**
572         * Set the {@code currencyTimeLimit} field to the specified
573         * "defaultCurrencyTimeLimit", if any (by default none).
574         * @param desc the JMX attribute or operation descriptor
575         * @see #setDefaultCurrencyTimeLimit(Integer)
576         */
577        protected final void applyDefaultCurrencyTimeLimit(Descriptor desc) {
578                if (getDefaultCurrencyTimeLimit() != null) {
579                        desc.setField(FIELD_CURRENCY_TIME_LIMIT, getDefaultCurrencyTimeLimit().toString());
580                }
581        }
582
583        /**
584         * Apply the given JMX "currencyTimeLimit" value to the given descriptor.
585         * <p>The default implementation sets a value {@code >0} as-is (as number of cache seconds),
586         * turns a value of {@code 0} into {@code Integer.MAX_VALUE} ("always cache")
587         * and sets the "defaultCurrencyTimeLimit" (if any, indicating "never cache") in case of
588         * a value {@code <0}. This follows the recommendation in the JMX 1.2 specification.
589         * @param desc the JMX attribute or operation descriptor
590         * @param currencyTimeLimit the "currencyTimeLimit" value to apply
591         * @see #setDefaultCurrencyTimeLimit(Integer)
592         * @see #applyDefaultCurrencyTimeLimit(javax.management.Descriptor)
593         */
594        protected void applyCurrencyTimeLimit(Descriptor desc, int currencyTimeLimit) {
595                if (currencyTimeLimit > 0) {
596                        // number of cache seconds
597                        desc.setField(FIELD_CURRENCY_TIME_LIMIT, Integer.toString(currencyTimeLimit));
598                }
599                else if (currencyTimeLimit == 0) {
600                        // "always cache"
601                        desc.setField(FIELD_CURRENCY_TIME_LIMIT, Integer.toString(Integer.MAX_VALUE));
602                }
603                else {
604                        // "never cache"
605                        applyDefaultCurrencyTimeLimit(desc);
606                }
607        }
608
609}