KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > org > mmbase > storage > StorageManagerFactory


1 /*
2
3 This software is OSI Certified Open Source Software.
4 OSI Certified is a certification mark of the Open Source Initiative.
5
6 The license (Mozilla version 1.0) can be read at the MMBase site.
7 See http://www.MMBase.org/license
8
9 */

10 package org.mmbase.storage;
11
12 import java.util.*;
13 import org.xml.sax.InputSource JavaDoc;
14
15 import org.mmbase.storage.search.SearchQueryHandler;
16 import org.mmbase.storage.util.*;
17
18 import org.mmbase.module.core.*;
19 import org.mmbase.clustering.ChangeManager;
20 import org.mmbase.clustering.MMBaseChangeInterface;
21 import org.mmbase.core.CoreField;
22
23 import org.mmbase.util.ResourceLoader;
24 import org.mmbase.util.transformers.CharTransformer;
25 import org.mmbase.util.transformers.Transformers;
26
27 import org.mmbase.util.logging.Logger;
28 import org.mmbase.util.logging.Logging;
29
30 /**
31  * This class contains functionality for retrieving StorageManager instances, which give access to the storage device.
32  * It also provides functionality for setting and retrieving configuration data.
33  * This is an abstract class. You cannot instantiate it. Use the static {@link #newInstance()} or {@link #newInstance(MMBase)}
34  * methods to obtain a factory class.
35  *
36  * @author Pierre van Rooden
37  * @since MMBase-1.7
38  * @version $Id: StorageManagerFactory.java,v 1.27 2006/07/15 10:58:36 michiel Exp $
39  */

40 public abstract class StorageManagerFactory {
41
42     private static final Logger log = Logging.getLoggerInstance(StorageManagerFactory.class);
43
44     /**
45      * A reference to the MMBase module
46      */

47     protected MMBase mmbase;
48     /**
49      * The class used to instantiate storage managers.
50      * The classname is retrieved from the storage configuration file
51      */

52     protected Class JavaDoc storageManagerClass;
53
54     /**
55      * The map with configuration data
56      */

57     protected Map attributes;
58
59     /**
60      * The list with type mappings
61      */

62     protected List typeMappings;
63
64     /**
65      * The ChangeManager object, used to register/broadcast changes to a node or set of nodes.
66      */

67     protected ChangeManager changeManager;
68
69
70     /**
71      * The map with disallowed fieldnames and (if given) alternates
72      */

73     protected final SortedMap disallowedFields = new TreeMap(String.CASE_INSENSITIVE_ORDER);
74
75     /**
76      * The query handler to use with this factory.
77      * Note: the current handler makes use of the JDBC2NodeInterface and is not optimized for storage: using it means
78      * you call getNodeManager() TWICE.
79      * Have to look into how this should work together.
80      */

81     protected SearchQueryHandler queryHandler;
82
83     /**
84      * The query handler classes.
85      * Assign a value to this class if you want to set a default query handler.
86      */

87     protected List queryHandlerClasses = new ArrayList();
88
89     /**
90      * @see #getSetSurrogator()
91      */

92     protected CharTransformer setSurrogator = null;
93
94     /**
95      * @see #getGetSurrogator()
96      */

97     protected CharTransformer getSurrogator = null;
98
99
100     /**
101      * The default storage factory class.
102      * This classname is used if you doe not spevify the clasanme in the 'storagemanagerfactory' proeprty in mmabseroot.xml.
103      */

104     static private final Class JavaDoc DEFAULT_FACTORY_CLASS = org.mmbase.storage.implementation.database.DatabaseStorageManagerFactory.class;
105
106     /**
107      * Obtain the StorageManagerFactory belonging to the indicated MMBase module.
108      * @param mmbase The MMBase module for which to retrieve the storagefactory
109      * @return The StorageManagerFactory
110      * @throws StorageException if the StorageManagerFactory class cannot be located, accessed, or instantiated,
111      * or when something went wrong during configuration of the factory
112      */

113     static public StorageManagerFactory newInstance(MMBase mmbase)
114                   throws StorageException {
115         // get the class name for the factory to instantiate
116
String JavaDoc factoryClassName = mmbase.getInitParameter("storagemanagerfactory");
117         // instantiate and initialize the class
118
try {
119             Class JavaDoc factoryClass = DEFAULT_FACTORY_CLASS;
120             if (factoryClassName != null) {
121                 factoryClass = Class.forName(factoryClassName);
122             }
123             StorageManagerFactory factory = (StorageManagerFactory)factoryClass.newInstance();
124             factory.init(mmbase);
125             return factory;
126         } catch (ClassNotFoundException JavaDoc cnfe) {
127             throw new StorageFactoryException(cnfe);
128         } catch (IllegalAccessException JavaDoc iae) {
129             throw new StorageFactoryException(iae);
130         } catch (InstantiationException JavaDoc ie) {
131             throw new StorageFactoryException(ie);
132         }
133     }
134
135     /**
136      * Obtain the storage manager factory belonging to the default MMBase module.
137      * @return The StoragemanagerFactory
138      * @throws StorageException if the StorageManagerFactory class cannot be located, accessed, or instantiated,
139      * or when something went wrong during configuration of the factory
140      */

141     static public StorageManagerFactory newInstance() throws StorageException {
142         // determine the default mmbase module.
143
return newInstance(MMBase.getMMBase());
144     }
145
146     /**
147      * Initialize the StorageManagerFactory.
148      * This method should be called after instantiation of the factory class.
149      * It is called automatically by {@link #newInstance()} and {@link #newInstance(MMBase)}.
150      * @param mmbase the MMBase instance to which this factory belongs
151      * @throws StorageError when something went wrong during configuration of the factory, or when the storage cannot be accessed
152      */

153     protected final void init(MMBase mmbase) throws StorageError {
154         log.service("initializing Storage Manager factory " + this.getClass().getName());
155         this.mmbase = mmbase;
156         attributes = Collections.synchronizedMap(new HashMap()); // ConcurrentHashMap not possible because null-values are put (TODO)
157
typeMappings = Collections.synchronizedList(new ArrayList()); // CopyOnWriteArrayList not possible because Collections.sort is done (TODO)
158
changeManager = new ChangeManager();
159         try {
160             log.debug("loading Storage Manager factory " + this.getClass().getName());
161             load();
162         } catch (StorageException se) {
163             // pass exceptions as a StorageError to signal a serious (unrecoverable) error condition
164
log.fatal(se.getMessage() + Logging.stackTrace(se));
165             throw new StorageError(se);
166         }
167     }
168
169     /**
170      * Return the MMBase module for which the factory was instantiated
171      * @return the MMBase instance
172      */

173     public MMBase getMMBase() {
174         return mmbase;
175     }
176
177     /**
178      * Instantiate a basic handler object using the specified class.
179      * A basic handler can be any type of class and is dependent on the
180      * factory implementation.
181      * For instance, the database factory expects an
182      * org.mmbase.storage.search.implentation.database.SQLHandler class.
183      * @param handlerClass the class to instantuate teh object with
184      * @return the new handler class
185      */

186     abstract protected Object JavaDoc instantiateBasicHandler(Class JavaDoc handlerClass);
187
188     /**
189      * Instantiate a chained handler object using the specified class.
190      * A chained handler can be any type of class and is dependent on the
191      * factory implementation.
192      * For instance, the database factory expects an
193      * org.mmbase.storage.search.implentation.database.ChainedSQLHandler class.
194      * @param handlerClass the class to instantuate teh object with
195      * @param previousHandler a handler thatw a sinstantiated previously.
196      * this handler should be passed to the new handler class during or
197      * after constrcution, so the ne whandler can 'chain' any events it cannot
198      * handle to this class.
199      * @return the new handler class
200      */

201     abstract protected Object JavaDoc instantiateChainedHandler(Class JavaDoc handlerClass, Object JavaDoc previousHandler);
202
203     /**
204      * Instantiate a SearchQueryHandler object using the specified object.
205      * The specified parameter may be an actual SearchQueryHandler object, or it may be a utility class.
206      * For instance, the database factory expects an org.mmbase.storage.search.implentation.database.SQLHandler object,
207      * which is used as a parameter in the construction of the actual SearchQueryHandler class.
208      * @param data the object to instantuate a SearchQueryHandler object with
209      */

210     abstract protected SearchQueryHandler instantiateQueryHandler(Object JavaDoc data);
211
212     /**
213      * Opens and reads the storage configuration document.
214      * Override this method to add additional configuration code before or after the configuration document is read.
215      * @throws StorageException if the storage could not be accessed or necessary configuration data is missing or invalid
216      */

217     protected void load() throws StorageException {
218         StorageReader reader = getDocumentReader();
219         if (reader == null) {
220             if (storageManagerClass == null || queryHandlerClasses.size() == 0) {
221                 throw new StorageConfigurationException("No storage reader specified, and no default values available.");
222             } else {
223                 log.warn("No storage reader specified, continue using default values.");
224                 log.debug("Default storage manager : " + storageManagerClass.getName());
225                 log.debug("Default query handler : " + ((Class JavaDoc)queryHandlerClasses.get(0)).getName());
226                 return;
227             }
228         }
229
230         // get the storage manager class
231
Class JavaDoc configuredClass = reader.getStorageManagerClass();
232         if (configuredClass != null) {
233             storageManagerClass = configuredClass;
234         } else if (storageManagerClass == null) {
235             throw new StorageConfigurationException("No StorageManager class specified, and no default available.");
236         }
237
238         // get attributes
239
setAttributes(reader.getAttributes());
240
241
242         // get disallowed fields, and add these to the default list
243
disallowedFields.putAll(reader.getDisallowedFields());
244
245         // add default replacements when DEFAULT_STORAGE_IDENTIFIER_PREFIX is given
246
String JavaDoc prefix = (String JavaDoc)getAttribute(Attributes.DEFAULT_STORAGE_IDENTIFIER_PREFIX);
247         if (prefix !=null) {
248             for (Iterator i = disallowedFields.entrySet().iterator(); i.hasNext();) {
249                 Map.Entry e = (Map.Entry)i.next();
250                 String JavaDoc name = (String JavaDoc) e.getKey();
251                 String JavaDoc replacement = (String JavaDoc) e.getValue();
252                 if (replacement == null ) {
253                     e.setValue(prefix + "_" + name);
254                 }
255             }
256         }
257
258         log.service("get type mappings");
259         typeMappings.addAll(reader.getTypeMappings());
260         Collections.sort(typeMappings);
261
262         // get the queryhandler class
263
// has to be done last, as we have to passing the disallowedfields map (doh!)
264
// need to move this to DatabaseStorageManagerFactory
265
List configuredClasses = reader.getSearchQueryHandlerClasses();
266         if (configuredClasses.size() != 0) {
267             queryHandlerClasses = configuredClasses;
268         } else if (queryHandlerClasses.size() == 0) {
269             throw new StorageConfigurationException("No SearchQueryHandler class specified, and no default available.");
270         }
271         log.service("Found queryhandlers " + queryHandlerClasses);
272         // instantiate handler(s)
273
Iterator iHandlers = reader.getSearchQueryHandlerClasses().iterator();
274         Object JavaDoc handler = null;
275         while (iHandlers.hasNext()) {
276             Class JavaDoc handlerClass = (Class JavaDoc) iHandlers.next();
277             if (handler == null) {
278                 handler = instantiateBasicHandler(handlerClass);
279             } else {
280                 handler = instantiateChainedHandler(handlerClass, handler);
281             }
282         }
283         // initialize query handler.
284
queryHandler = instantiateQueryHandler(handler);
285
286         String JavaDoc surr = (String JavaDoc) getAttribute(Attributes.SET_SURROGATOR);
287         if (surr != null && ! surr.equals("")) {
288             setSurrogator = Transformers.getCharTransformer(surr, null, "StorageManagerFactory#load", false);
289         }
290
291         surr = (String JavaDoc) getAttribute(Attributes.GET_SURROGATOR);
292         if (surr != null && ! surr.equals("")) {
293             getSurrogator = Transformers.getCharTransformer(surr, null, "StorageManagerFactory#load", false);
294         }
295     }
296
297     /**
298      * Obtains a StorageManager from the factory.
299      * The instance represents a temporary connection to the datasource -
300      * do not store the result of this call as a static or long-term member of a class.
301      * @return a StorageManager instance
302      * @throws StorageException when the storagemanager cannot be created
303      */

304     public StorageManager getStorageManager() throws StorageException {
305         try {
306             StorageManager storageManager = (StorageManager)storageManagerClass.newInstance();
307             storageManager.init(this);
308             return storageManager;
309         } catch(InstantiationException JavaDoc ie) {
310             throw new StorageException(ie);
311         } catch(IllegalAccessException JavaDoc iae) {
312             throw new StorageException(iae);
313         }
314     }
315
316     // javadoc inherited
317
/**
318      * Obtains a SearchQueryHandler from the factory.
319      * This provides ways to query for data using the SearchQuery interface.
320      * Note that cannot run the querys on a transaction (since SearchQuery does not support them).
321      *
322      * @return a SearchQueryHandler instance
323      * @throws StorageException when the handler cannot be created
324      */

325     public SearchQueryHandler getSearchQueryHandler() throws StorageException {
326         if (queryHandler==null) {
327             throw new StorageException("Cannot obtain a query handler.");
328         } else {
329             return queryHandler;
330         }
331     }
332
333     /**
334      * Locates and opens the storage configuration document, if available.
335      * The configuration document to open can be set in mmbasereoot (using the storage property).
336      * The property should point to a resource which is to be present in the MMBase classpath.
337      * Overriding factories may provide ways to auto-detect the location of a configuration file.
338      * @throws StorageException if something went wrong while obtaining the document reader
339      * @return a StorageReader instance, or null if no reader has been configured
340      */

341     public StorageReader getDocumentReader() throws StorageException {
342         // determine storage resource.
343
String JavaDoc storagePath = mmbase.getInitParameter("storage");
344         // use the parameter set in mmbaseroot if it is given
345
if (storagePath != null) {
346             try {
347                 InputSource JavaDoc resource = ResourceLoader.getConfigurationRoot().getInputSource(storagePath);
348                 if (resource == null) {
349                     throw new StorageConfigurationException("Storage resource '" + storagePath + "' not found.");
350                 }
351                 return new StorageReader(this, resource);
352             } catch (java.io.IOException JavaDoc ioe) {
353                 throw new StorageConfigurationException(ioe);
354             }
355         } else {
356             // otherwise return null
357
return null;
358         }
359     }
360
361     /**
362      * Retrieve a map of attributes for this factory.
363      * The attributes are the configuration parameters for the factory.
364      * You cannot change this map, though you can change the attributes themselves.
365      * @return an unmodifiable Map
366      */

367     public Map getAttributes() {
368         return Collections.unmodifiableMap(attributes);
369     }
370
371     /**
372      * Add a map of attributes for this factory.
373      * The attributes are the configuration parameters for the factory.
374      * The actual content the factory expects is dependent on the implementation.
375      * The attributes are added to any attributes already knwon to the factory.
376      * @param attributes the map of attributes to add
377      */

378     public void setAttributes(Map attributes) {
379         this.attributes.putAll(attributes);
380         log.debug("Database attributes " + this.attributes);
381     }
382
383     /**
384      * Obtain an attribute from this factory.
385      * Attributes are the configuration parameters for the storagefactory.
386      * @param key the key of the attribute
387      * @return the attribute value, or null if it is unknown
388      */

389     public Object JavaDoc getAttribute(Object JavaDoc key) {
390         return attributes.get(key);
391     }
392
393     /**
394      * Set an attribute of this factory.
395      * Attributes are the configuration parameters for the factory.
396      * The actual content the factory expects is dependent on the implementation.
397      * To invalidate an attribute, you can pass the <code>null</code> value.
398      * @param key the key of the attribute
399      * @param value the value of the attribute
400      */

401     public void setAttribute(Object JavaDoc key, Object JavaDoc value) {
402         attributes.put(key, value);
403     }
404
405     /**
406      * Obtain a scheme from this factory.
407      * Schemes are special attributes, consisting of patterned strings that can be
408      * expanded with arguments.
409      * @param key the key of the attribute
410      * @return the scheme value, or null if it is unknown
411      */

412     public Scheme getScheme(Object JavaDoc key) {
413         return getScheme(key, null);
414     }
415
416     /**
417      * Obtain a scheme from this factory.
418      * Schemes are special attributes, consisting of patterned strings that can be
419      * expanded with arguments.
420      * If no scheme is present, the default pattern is used to create a scheme and add it to the factory.
421      * @param key the key of the attribute
422      * @param defaultPattern the pattern to use for the default scheme, <code>null</code> if there is no default
423      * @return the scheme value, <code>null</code> if there is no scheme
424      */

425     public Scheme getScheme(Object JavaDoc key, String JavaDoc defaultPattern) {
426         Scheme scheme =(Scheme)getAttribute(key);
427         if (scheme == null && defaultPattern != null) {
428             if (attributes.containsKey(key)) return null;
429             scheme = new Scheme(this,defaultPattern);
430             setAttribute(key,scheme);
431         }
432         return scheme;
433     }
434
435     /**
436      * Set a scheme of this factory, using a string pattern to base the Scheme on.
437      * Schemes are special attributes, consisting of patterned strings that can be
438      * expanded with arguments.
439      * @param key the key of the scheme
440      * @param pattern the pattern to use for the scheme
441      */

442     public void setScheme(Object JavaDoc key, String JavaDoc pattern) {
443         if (pattern == null || pattern.equals("")) {
444             setAttribute(key,null);
445         } else {
446             setAttribute(key,new Scheme(this,pattern));
447         }
448     }
449
450     /**
451      * Check whether an option was set.
452      * Options are attributes that return a boolean value.
453      * @param key the key of the option
454      * @return <code>true</code> if the option was set
455      */

456     public boolean hasOption(Object JavaDoc key) {
457         Object JavaDoc o = getAttribute(key);
458         return (o instanceof Boolean JavaDoc) && ((Boolean JavaDoc)o).booleanValue();
459     }
460
461     /**
462      * Set an option to true or false.
463      * @param key the key of the option
464      * @param value the value of the option (true or false)
465      */

466     public void setOption(Object JavaDoc key, boolean value) {
467         setAttribute(key, Boolean.valueOf(value));
468     }
469
470     /**
471      * Returns a sorted list of type mappings for this storage.
472      * @return the list of TypeMapping objects
473      */

474     public List getTypeMappings() {
475         return Collections.unmodifiableList(typeMappings);
476     }
477
478     /**
479      * Returns a map of disallowed field names and their possible alternate values.
480      * @return A Map of disallowed field names
481      */

482     public Map getDisallowedFields() {
483         return Collections.unmodifiableSortedMap(disallowedFields);
484     }
485
486     /**
487      * Sets the map of disallowed Fields.
488      * Unlike setAttributes(), this actually replaces the existing disallowed fields map.
489      */

490     protected void setDisallowedFields(Map disallowedFields) {
491         this.disallowedFields.clear();
492         this.disallowedFields.putAll(disallowedFields);
493     }
494
495     /**
496      * Obtains the identifier for the basic storage element.
497      * @return the storage-specific identifier
498      * @throws StorageException if the object cannot be given a valid identifier
499      */

500     public Object JavaDoc getStorageIdentifier() throws StorageException {
501         return getStorageIdentifier(mmbase);
502     }
503
504     /**
505      * Obtains a identifier for an MMBase object.
506      * The default implementation returns the following type of identifiers:
507      * <ul>
508      * <li>For StorageManager: the basename</li>
509      * <li>For MMBase: the String '[basename]_object</li>
510      * <li>For MMObjectBuilder: the String '[basename]_[builder name]'</li>
511      * <li>For Indices: the String '[basename]_[builder name]_[index name]_idx'</li>
512      * <li>For MMObjectNode: the object number as a Integer</li>
513      * <li>For CoreField or String: the field name, or the replacement fieldfname (from the disallowedfields map)</li>
514      * </ul>
515      * Note that 'basename' is a property from the mmbase module, set in mmbaseroot.xml.<br />
516      * You can override this method if you intend to use different ids.
517      *
518      * @see Storable
519      * @param mmobject the MMBase objecty
520      * @return the storage-specific identifier
521      * @throws StorageException if the object cannot be given a valid identifier
522      */

523     public Object JavaDoc getStorageIdentifier(Object JavaDoc mmobject) throws StorageException {
524         String JavaDoc id;
525         if (mmobject instanceof StorageManager) {
526             id = mmbase.getBaseName();
527         } else if (mmobject == mmbase) {
528             id = mmbase.getBaseName()+"_object";
529         } else if (mmobject instanceof MMObjectBuilder) {
530             id = mmbase.getBaseName()+"_"+((MMObjectBuilder)mmobject).getTableName();
531         } else if (mmobject instanceof MMObjectNode) {
532             return ((MMObjectNode)mmobject).getIntegerValue("number");
533         } else if (mmobject instanceof Index) {
534             id = mmbase.getBaseName()+"_"+((Index)mmobject).getParent().getTableName() + "_" + ((Index)mmobject).getName() + "_idx";
535         } else if (mmobject instanceof String JavaDoc || mmobject instanceof CoreField) {
536             if (mmobject instanceof CoreField) {
537                 id = ((CoreField)mmobject).getName();
538             } else {
539                 id = (String JavaDoc)mmobject;
540             }
541             String JavaDoc key = id;
542             if (!hasOption(Attributes.DISALLOWED_FIELD_CASE_SENSITIVE)) {
543                 key = key.toLowerCase();
544             }
545             if (disallowedFields.containsKey(key)) {
546                 String JavaDoc newid = (String JavaDoc)disallowedFields.get(key);
547                 if (newid == null) {
548                     if (hasOption(Attributes.ENFORCE_DISALLOWED_FIELDS)) {
549                         throw new StorageException("The name of the field '"+((CoreField)mmobject).getName()+"' is disallowed, and no alternate value is available.");
550                     }
551                 } else {
552                     id = newid;
553                 }
554             }
555         } else {
556             throw new StorageException("Cannot obtain identifier for param of type '"+mmobject.getClass().getName()+".");
557         }
558
559         String JavaDoc maxIdentifierLength = (String JavaDoc)getAttribute(Attributes.MAX_IDENTIFIER_LENGTH);
560         if (maxIdentifierLength != null && !"".equals(maxIdentifierLength)) {
561           try {
562             int maxlength = Integer.parseInt(maxIdentifierLength);
563             if (id.length() > maxlength) {
564               // Truncate the id, leave 8 characters space to put the hashcode
565
String JavaDoc base = id.substring(0, maxlength - 8);
566               long hashcode = id.hashCode();
567               if (hashcode < 0) hashcode = Integer.MAX_VALUE + hashcode;
568
569               // This generates a 8-character hex representation of the strings hashcode
570
id = base + (new java.math.BigInteger JavaDoc(""+hashcode)).toString(16);
571             }
572           } catch (NumberFormatException JavaDoc e) {
573             log.warn("Exception parsing the 'max-identifier-length' parameter; ignoring it!");
574           }
575         }
576
577         String JavaDoc toCase = (String JavaDoc)getAttribute(Attributes.STORAGE_IDENTIFIER_CASE);
578         if ("lower".equals(toCase)) {
579             return id.toLowerCase();
580         } else if ("upper".equals(toCase)) {
581             return id.toUpperCase();
582         } else {
583             return id;
584         }
585     }
586
587     /**
588      * Returns the ChangeManager utility instance, used to register and broadcast changes to nodes
589      * in the storage layer.
590      * This method is for use by the StorageManager
591      */

592     public ChangeManager getChangeManager() {
593         return changeManager;
594     }
595
596     /**
597      * Returns the name of the catalog used by this storage (<code>null</code> if no catalog is used).
598      */

599     public String JavaDoc getCatalog() {
600         return null;
601     }
602
603     /**
604      * Returns the version of this factory implementation.
605      * The factory uses this number to verify whether it can handle storage configuration files
606      * that list version requirements.
607      * @return the version as an integer
608      */

609     abstract public double getVersion();
610
611     /**
612      * Returns whether transactions, and specifically rollback, is supported in the storage layer.
613      * @return <code>true</code> if trasnactions are supported
614      */

615     abstract public boolean supportsTransactions();
616
617
618
619     /**
620      * Returns a filter which can be used to filter strings taken from storage or <code>null</code> if none defined.
621      * @since MMBase-1.7.4
622      */

623     public CharTransformer getGetSurrogator() {
624         return getSurrogator;
625     }
626     /**
627      * Returns a filter which can be used to filter strings which are to be set into storage or <code>null</code> if none defined.
628      * @since MMBase-1.7.4
629      */

630     public CharTransformer getSetSurrogator() {
631         return setSurrogator;
632     }
633
634
635     /**
636      * Returns the offset which must be used in the database. Currently this is based on the system's
637      * default time zone. It is imaginable that can have configuration or database specific details later.
638      * @param time The time at which it is evaluated (summer time issues)
639      * @since MMBase-1.8
640      * @todo experimental
641      */

642     public int getTimeZoneOffset(long time) {
643         return TimeZone.getDefault().getOffset(time);
644     }
645
646
647 }
648
Popular Tags