001    package org.apache.ojb.broker.metadata;
002    
003    /* Copyright 2002-2005 The Apache Software Foundation
004     *
005     * Licensed under the Apache License, Version 2.0 (the "License");
006     * you may not use this file except in compliance with the License.
007     * You may obtain a copy of the License at
008     *
009     *     http://www.apache.org/licenses/LICENSE-2.0
010     *
011     * Unless required by applicable law or agreed to in writing, software
012     * distributed under the License is distributed on an "AS IS" BASIS,
013     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014     * See the License for the specific language governing permissions and
015     * limitations under the License.
016     */
017    
018    import java.io.FileNotFoundException;
019    import java.io.InputStream;
020    import java.util.Hashtable;
021    import java.util.Iterator;
022    import java.util.List;
023    
024    import org.apache.commons.lang.SerializationUtils;
025    import org.apache.ojb.broker.PBKey;
026    import org.apache.ojb.broker.core.PersistenceBrokerConfiguration;
027    import org.apache.ojb.broker.util.configuration.impl.OjbConfigurator;
028    import org.apache.ojb.broker.util.logging.Logger;
029    import org.apache.ojb.broker.util.logging.LoggerFactory;
030    
031    /**
032     * Central class for metadata operations/manipulations - manages OJB's
033     * metadata objects, in particular:
034     * <ul>
035     * <li>{@link org.apache.ojb.broker.metadata.DescriptorRepository} contains
036     * metadata of persistent objects</li>
037     * <li>{@link org.apache.ojb.broker.metadata.ConnectionRepository} contains
038     * all connection metadata information</li>
039     * </ul>
040     *
041     * This class allows transparent flexible metadata loading/manipulation at runtime.
042     *
043     * <p>
044     * <b>How to read/merge metadata</b><br/>
045     * Per default OJB loads default {@link org.apache.ojb.broker.metadata.DescriptorRepository}
046     * and {@link org.apache.ojb.broker.metadata.ConnectionRepository} instances, by reading the
047     * specified repository file. This is done first time the <code>MetadataManager</code> instance
048     * was used.
049     * <br/>
050     * To read metadata information at runtime use
051     * {@link #readDescriptorRepository readDescriptorRepository} and
052     * {@link #readConnectionRepository readConnectionRepository}
053     * methods.
054     * <br/>
055     * It is also possible to merge different repositories using
056     * {@link #mergeDescriptorRepository mergeDescriptorRepository}
057     * and {@link #mergeConnectionRepository mergeConnectionRepository}
058     *
059     * </p>
060     *
061     * <a name="perThread"/>
062     * <h3>Per thread handling of metadata</h3>
063     * <p>
064     * Per default the manager handle one global {@link org.apache.ojb.broker.metadata.DescriptorRepository}
065     * for all calling threads, but it is ditto possible to use different metadata <i>profiles</i> in a per thread
066     * manner - <i>profiles</i> means different copies of {@link org.apache.ojb.broker.metadata.DescriptorRepository}
067     * objects.
068     * <p/>
069     *
070     * <p>
071     * <a name="enablePerThreadMode"/>
072     * <b>Enable the per thread mode</b><br/>
073     * To enable the 'per thread' mode for {@link org.apache.ojb.broker.metadata.DescriptorRepository}
074     * instances:
075     * <pre>
076     *   MetadataManager mm = MetadataManager.getInstance();
077     *   // tell the manager to use per thread mode
078     *   mm.setEnablePerThreadChanges(true);
079     *   ...
080     * </pre>
081     * This could be done e.g. at start up.<br/>
082     * Now it's possible to use dedicated <code>DescriptorRepository</code> instances
083     * per thread:
084     *  <pre>
085     *   // e.g we get a coppy of the global repository
086     *   DescriptorRepository dr = mm.copyOfGlobalRepository();
087     *   // now we can manipulate the persistent object metadata of the copy
088     *   ......
089     *
090     *   // set the changed repository for this thread
091     *   mm.setDescriptor(dr);
092     *
093     *   // now let this thread lookup a PersistenceBroker instance
094     *   // with the modified metadata
095     *   // all other threads use the global metadata
096     *   PersistenceBroker broker = Persis......
097     * </pre>
098     * Note: Change metadata <i>before</i> lookup the {@link org.apache.ojb.broker.PersistenceBroker}
099     * instance for current thread, because the metadata was bound to the PB at lookup.
100     * </p>
101     *
102     * <p>
103     * <b>How to use different metadata profiles</b><br/>
104     * MetadataManager was shipped with a simple mechanism to
105     * add, remove and load different persistent objects metadata
106     * profiles (different {@link org.apache.ojb.broker.metadata.DescriptorRepository}
107     * instances) in a per thread manner. Use
108     * <ul>
109     * <li>{@link #addProfile addProfile} add different persistent object metadata profiles</li>
110     * <li>{@link #removeProfile removeProfile} remove a persistent object metadata profiles</li>
111     * <li>{@link #loadProfile loadProfile} load a profile for the current thread</li>
112     * </ul>
113     * Note: method {@link #loadProfile loadProfile} only works if
114     * the <a href="#enablePerThreadMode">per thread mode</a> is enabled.
115     * </p>
116     *
117     *
118     * @author <a href="mailto:armin@codeAuLait.de">Armin Waibel</a>
119     * @version $Id: MetadataManager.java,v 1.1 2007-08-24 22:17:29 ewestfal Exp $
120     */
121    public class MetadataManager
122    {
123        private static Logger log = LoggerFactory.getLogger(MetadataManager.class);
124    
125        private static final String MSG_STR = "* Can't find DescriptorRepository for current thread, use default one *";
126        private static ThreadLocal threadedRepository = new ThreadLocal();
127        private static ThreadLocal currentProfileKey = new ThreadLocal();
128        private static MetadataManager singleton;
129    
130        private Hashtable metadataProfiles;
131        private DescriptorRepository globalRepository;
132        private ConnectionRepository connectionRepository;
133        private boolean enablePerThreadChanges;
134        private PBKey defaultPBKey;
135    
136        // singleton
137        private MetadataManager()
138        {
139            init();
140        }
141    
142        private void init()
143        {
144            metadataProfiles = new Hashtable();
145            final String repository = ((PersistenceBrokerConfiguration) OjbConfigurator.getInstance()
146                    .getConfigurationFor(null)).getRepositoryFilename();
147            try
148            {
149                globalRepository     = new RepositoryPersistor().readDescriptorRepository(repository);
150                connectionRepository = new RepositoryPersistor().readConnectionRepository(repository);
151            }
152            catch (FileNotFoundException ex)
153            {
154                log.warn("Could not access '" + repository + "' or a DOCTYPE/DTD-dependency. "
155                         + "(Check letter case for file names and HTTP-access if using DOCTYPE PUBLIC)"
156                         + " Starting with empty metadata and connection configurations.", ex);
157                globalRepository     = new DescriptorRepository();
158                connectionRepository = new ConnectionRepository();
159            }
160            catch (Exception ex)
161            {
162                throw new MetadataException("Can't read repository file '" + repository + "'", ex);
163            }
164        }
165    
166        public void shutdown()
167        {
168            threadedRepository = null;
169            currentProfileKey = null;
170            globalRepository = null;
171            metadataProfiles = null;
172            singleton = null;
173        }
174    
175        /**
176         * Returns an instance of this class.
177         */
178        public static synchronized MetadataManager getInstance()
179        {
180            // lazy initialization
181            if (singleton == null)
182            {
183                singleton = new MetadataManager();
184            }
185            return singleton;
186        }
187    
188        /**
189         * Returns the current valid {@link org.apache.ojb.broker.metadata.DescriptorRepository} for
190         * the caller. This is the provided way to obtain the
191         * {@link org.apache.ojb.broker.metadata.DescriptorRepository}.
192         * <br>
193         * When {@link #isEnablePerThreadChanges per thread descriptor handling}  is enabled
194         * it search for a specific {@link org.apache.ojb.broker.metadata.DescriptorRepository}
195         * for the calling thread, if none can be found the global descriptor was returned.
196         *
197         * @see MetadataManager#getGlobalRepository
198         * @see MetadataManager#copyOfGlobalRepository
199         */
200        public DescriptorRepository getRepository()
201        {
202            DescriptorRepository repository;
203            if (enablePerThreadChanges)
204            {
205                repository = (DescriptorRepository) threadedRepository.get();
206                if (repository == null)
207                {
208                    repository = getGlobalRepository();
209                    log.info(MSG_STR);
210                }
211    // arminw:
212    // TODO: Be more strict in per thread mode and throw a exception when not find descriptor for calling thread?
213    //            if (repository == null)
214    //            {
215    //                throw new MetadataException("Can't find a DescriptorRepository for current thread, don't forget" +
216    //                        " to set a DescriptorRepository if enable per thread changes before perform other action");
217    //            }
218                return repository;
219            }
220            else
221            {
222                return globalRepository;
223            }
224        }
225    
226        /**
227         * Returns explicit the global {@link org.apache.ojb.broker.metadata.DescriptorRepository} - use with
228         * care, because it ignores the {@link #isEnablePerThreadChanges per thread mode}.
229         *
230         * @see MetadataManager#getRepository
231         * @see MetadataManager#copyOfGlobalRepository
232         */
233        public DescriptorRepository getGlobalRepository()
234        {
235            return globalRepository;
236        }
237    
238        /**
239         * Returns the {@link ConnectionRepository}.
240         */
241        public ConnectionRepository connectionRepository()
242        {
243            return connectionRepository;
244        }
245    
246        /**
247         * Merge the given {@link ConnectionRepository} with the existing one (without making
248         * a deep copy of the containing connection descriptors).
249         * @see #mergeConnectionRepository(ConnectionRepository targetRepository, ConnectionRepository sourceRepository, boolean deep)
250         */
251        public void mergeConnectionRepository(ConnectionRepository repository)
252        {
253            mergeConnectionRepository(connectionRepository(), repository, false);
254        }
255    
256        /**
257         * Merge the given source {@link ConnectionRepository} with the
258         * existing target. If parameter
259         * <tt>deep</tt> is set <code>true</code> deep copies of source objects were made.
260         * <br/>
261         * Note: Using <tt>deep copy mode</tt> all descriptors will be serialized
262         * by using the default class loader to resolve classes. This can be problematic
263         * when classes are loaded by a context class loader.
264         * <p>
265         * Note: All classes within the repository structure have to implement
266         * <code>java.io.Serializable</code> to be able to create a cloned copy.
267         */
268        public void mergeConnectionRepository(
269                ConnectionRepository targetRepository, ConnectionRepository sourceRepository, boolean deep)
270        {
271            List list = sourceRepository.getAllDescriptor();
272            for (Iterator iterator = list.iterator(); iterator.hasNext();)
273            {
274                JdbcConnectionDescriptor jcd = (JdbcConnectionDescriptor) iterator.next();
275                if (deep)
276                {
277                    //TODO: adopt copy/clone methods for metadata classes?
278                    jcd = (JdbcConnectionDescriptor) SerializationUtils.clone(jcd);
279                }
280                targetRepository.addDescriptor(jcd);
281            }
282        }
283    
284        /**
285         * Merge the given {@link org.apache.ojb.broker.metadata.DescriptorRepository}
286         * (without making a deep copy of containing class-descriptor objects) with the
287         * global one, returned by method {@link #getRepository()} - keep
288         * in mind if running in <a href="#perThread">per thread mode</a>
289         * merge maybe only takes effect on current thread.
290         *
291         * @see #mergeDescriptorRepository(DescriptorRepository targetRepository, DescriptorRepository sourceRepository, boolean deep)
292         */
293        public void mergeDescriptorRepository(DescriptorRepository repository)
294        {
295            mergeDescriptorRepository(getRepository(), repository, false);
296        }
297    
298        /**
299         * Merge the given {@link org.apache.ojb.broker.metadata.DescriptorRepository}
300         * files, the source objects will be pushed to the target repository. If parameter
301         * <tt>deep</tt> is set <code>true</code> deep copies of source objects were made.
302         * <br/>
303         * Note: Using <tt>deep copy mode</tt> all descriptors will be serialized
304         * by using the default class loader to resolve classes. This can be problematic
305         * when classes are loaded by a context class loader.
306         * <p>
307         * Note: All classes within the repository structure have to implement
308         * <code>java.io.Serializable</code> to be able to create a cloned copy.
309         *
310         * @see #isEnablePerThreadChanges
311         * @see #setEnablePerThreadChanges
312         */
313        public void mergeDescriptorRepository(
314                DescriptorRepository targetRepository, DescriptorRepository sourceRepository, boolean deep)
315        {
316            Iterator it = sourceRepository.iterator();
317            while (it.hasNext())
318            {
319                ClassDescriptor cld = (ClassDescriptor) it.next();
320                if (deep)
321                {
322                    //TODO: adopt copy/clone methods for metadata classes?
323                    cld = (ClassDescriptor) SerializationUtils.clone(cld);
324                }
325                targetRepository.put(cld.getClassOfObject(), cld);
326                cld.setRepository(targetRepository);
327            }
328        }
329    
330        /**
331         * Read ClassDescriptors from the given repository file.
332         * @see #mergeDescriptorRepository
333         */
334        public DescriptorRepository readDescriptorRepository(String fileName)
335        {
336            try
337            {
338                RepositoryPersistor persistor = new RepositoryPersistor();
339                return persistor.readDescriptorRepository(fileName);
340            }
341            catch (Exception e)
342            {
343                throw new MetadataException("Can not read repository " + fileName, e);
344            }
345        }
346    
347        /**
348         * Read ClassDescriptors from the given InputStream.
349         * @see #mergeDescriptorRepository
350         */
351        public DescriptorRepository readDescriptorRepository(InputStream inst)
352        {
353            try
354            {
355                RepositoryPersistor persistor = new RepositoryPersistor();
356                return persistor.readDescriptorRepository(inst);
357            }
358            catch (Exception e)
359            {
360                throw new MetadataException("Can not read repository " + inst, e);
361            }
362        }
363    
364        /**
365         * Read JdbcConnectionDescriptors from the given repository file.
366         *
367         * @see #mergeConnectionRepository
368         */
369        public ConnectionRepository readConnectionRepository(String fileName)
370        {
371            try
372            {
373                RepositoryPersistor persistor = new RepositoryPersistor();
374                return persistor.readConnectionRepository(fileName);
375            }
376            catch (Exception e)
377            {
378                throw new MetadataException("Can not read repository " + fileName, e);
379            }
380        }
381    
382        /**
383         * Read JdbcConnectionDescriptors from this InputStream.
384         *
385         * @see #mergeConnectionRepository
386         */
387        public ConnectionRepository readConnectionRepository(InputStream inst)
388        {
389            try
390            {
391                RepositoryPersistor persistor = new RepositoryPersistor();
392                return persistor.readConnectionRepository(inst);
393            }
394            catch (Exception e)
395            {
396                throw new MetadataException("Can not read repository from " + inst, e);
397            }
398        }
399    
400        /**
401         * Set the {@link org.apache.ojb.broker.metadata.DescriptorRepository} - if <i>global</i> was true, the
402         * given descriptor aquire global availability (<i>use with care!</i>),
403         * else the given descriptor was associated with the calling thread.
404         *
405         * @see #isEnablePerThreadChanges
406         * @see #setEnablePerThreadChanges
407         */
408        public void setDescriptor(DescriptorRepository repository, boolean global)
409        {
410            if (global)
411            {
412                if (log.isDebugEnabled()) log.debug("Set new global repository: " + repository);
413                globalRepository = repository;
414            }
415            else
416            {
417                if (log.isDebugEnabled()) log.debug("Set new threaded repository: " + repository);
418                threadedRepository.set(repository);
419            }
420        }
421    
422        /**
423         * Set {@link DescriptorRepository} for the current thread.
424         * Convenience method for
425         * {@link #setDescriptor(DescriptorRepository repository, boolean global) setDescriptor(repository, false)}.
426         */
427        public void setDescriptor(DescriptorRepository repository)
428        {
429            setDescriptor(repository, false);
430        }
431    
432        /**
433         * Convenience method for
434         * {@link #setDescriptor setDescriptor(repository, false)}.
435         * @deprecated use {@link #setDescriptor}
436         */
437        public void setPerThreadDescriptor(DescriptorRepository repository)
438        {
439            setDescriptor(repository, false);
440        }
441    
442        /**
443         * Returns a copy of the current global
444         * {@link org.apache.ojb.broker.metadata.DescriptorRepository}
445         * <p>
446         * Note: All classes within the repository structure have to implement
447         * <code>java.io.Serializable</code> to be able to create a cloned copy.
448         *
449         * @see MetadataManager#getGlobalRepository
450         * @see MetadataManager#getRepository
451         */
452        public DescriptorRepository copyOfGlobalRepository()
453        {
454            return (DescriptorRepository) SerializationUtils.clone(globalRepository);
455        }
456    
457        /**
458         * If returns <i>true</i> if <a href="#perThread">per thread</a> runtime
459         * changes of the {@link org.apache.ojb.broker.metadata.DescriptorRepository}
460         * is enabled and the {@link #getRepository} method returns a threaded
461         * repository file if set, or the global if no threaded was found.
462         * <br>
463         * If returns <i>false</i> the {@link #getRepository} method return
464         * always the {@link #getGlobalRepository() global} repository.
465         *
466         * @see #setEnablePerThreadChanges
467         */
468        public boolean isEnablePerThreadChanges()
469        {
470            return enablePerThreadChanges;
471        }
472    
473        /**
474         * Enable the possibility of making <a href="#perThread">per thread</a> runtime changes
475         * of the {@link org.apache.ojb.broker.metadata.DescriptorRepository}.
476         *
477         * @see #isEnablePerThreadChanges
478         */
479        public void setEnablePerThreadChanges(boolean enablePerThreadChanges)
480        {
481            this.enablePerThreadChanges = enablePerThreadChanges;
482        }
483    
484        /**
485         * Add a metadata profile.
486         * @see #loadProfile
487         */
488        public void addProfile(Object key, DescriptorRepository repository)
489        {
490            if (metadataProfiles.contains(key))
491            {
492                throw new MetadataException("Duplicate profile key. Key '" + key + "' already exists.");
493            }
494            metadataProfiles.put(key, repository);
495        }
496    
497        /**
498         * Load the given metadata profile for the current thread.
499         *
500         */
501        public void loadProfile(Object key)
502        {
503            if (!isEnablePerThreadChanges())
504            {
505                throw new MetadataException("Can not load profile with disabled per thread mode");
506            }
507            DescriptorRepository rep = (DescriptorRepository) metadataProfiles.get(key);
508            if (rep == null)
509            {
510                throw new MetadataException("Can not find profile for key '" + key + "'");
511            }
512            currentProfileKey.set(key);
513            setDescriptor(rep);
514        }
515    
516        /**
517         * Returns the last activated profile key.
518         * @return the last activated profile key or null if no profile has been loaded
519         * @throws MetadataException if per-thread changes has not been activated
520         * @see #loadProfile(Object)
521         */
522        public Object getCurrentProfileKey() throws MetadataException
523        {
524            if (!isEnablePerThreadChanges())
525            {
526                throw new MetadataException("Call to this method is undefined, since per-thread mode is disabled.");
527            }
528            return currentProfileKey.get();
529        }
530    
531        /**
532         * Remove the given metadata profile.
533         */
534        public DescriptorRepository removeProfile(Object key)
535        {
536            return (DescriptorRepository) metadataProfiles.remove(key);
537        }
538    
539        /**
540         * Remove all metadata profiles.
541         */
542        public void clearProfiles()
543        {
544            metadataProfiles.clear();
545            currentProfileKey.set(null);
546        }
547    
548        /**
549         * Remove all profiles
550         *
551         * @see #removeProfile
552         * @see #addProfile
553         */
554        public void removeAllProfiles()
555        {
556            metadataProfiles.clear();
557            currentProfileKey.set(null);
558        }
559    
560        /**
561         * Return the default {@link PBKey} used in convinience method
562         * {@link org.apache.ojb.broker.PersistenceBrokerFactory#defaultPersistenceBroker}.
563         * <br/>
564         * If in {@link JdbcConnectionDescriptor} the
565         * {@link JdbcConnectionDescriptor#isDefaultConnection() default connection}
566         * is enabled, OJB will detect the default {@link org.apache.ojb.broker.PBKey} by itself.
567         *
568         * @see #setDefaultPBKey
569         */
570        public PBKey getDefaultPBKey()
571        {
572            if(defaultPBKey == null)
573            {
574                defaultPBKey = buildDefaultKey();
575            }
576            return defaultPBKey;
577        }
578    
579        /**
580         * Set the {@link PBKey} used in convinience method
581         * {@link org.apache.ojb.broker.PersistenceBrokerFactory#defaultPersistenceBroker}.
582         * <br/>
583         * It's only allowed to use one {@link JdbcConnectionDescriptor} with enabled
584         * {@link JdbcConnectionDescriptor#isDefaultConnection() default connection}. In this case
585         * OJB will automatically set the default key.
586         * <br/>
587         * Note: It's recommended to set this key only once and not to change at runtime
588         * of OJB to avoid side-effects.
589         * If set more then one time a warning will be logged.
590         * @throws MetadataException if key was set more than one time
591         */
592        public void setDefaultPBKey(PBKey defaultPBKey)
593        {
594            if(this.defaultPBKey != null)
595            {
596                log.warn("The used default PBKey change. Current key is " + this.defaultPBKey + ", new key will be " + defaultPBKey);
597            }
598            this.defaultPBKey = defaultPBKey;
599            log.info("Set default PBKey for convenience broker creation: " + defaultPBKey);
600        }
601    
602        /**
603         * Try to build an default PBKey for convenience PB create method.
604         *
605         * @return PBKey or <code>null</code> if default key was not declared in
606         * metadata
607         */
608        private PBKey buildDefaultKey()
609        {
610            List descriptors = connectionRepository().getAllDescriptor();
611            JdbcConnectionDescriptor descriptor;
612            PBKey result = null;
613            for (Iterator iterator = descriptors.iterator(); iterator.hasNext();)
614            {
615                descriptor = (JdbcConnectionDescriptor) iterator.next();
616                if (descriptor.isDefaultConnection())
617                {
618                    if(result != null)
619                    {
620                        log.error("Found additional connection descriptor with enabled 'default-connection' "
621                                + descriptor.getPBKey() + ". This is NOT allowed. Will use the first found descriptor " + result
622                                + " as default connection");
623                    }
624                    else
625                    {
626                        result = descriptor.getPBKey();
627                    }
628                }
629            }
630    
631            if(result == null)
632            {
633                log.info("No 'default-connection' attribute set in jdbc-connection-descriptors," +
634                        " thus it's currently not possible to use 'defaultPersistenceBroker()' " +
635                        " convenience method to lookup PersistenceBroker instances. But it's possible"+
636                        " to enable this at runtime using 'setDefaultKey' method.");
637            }
638            return result;
639        }
640    }