001: /*
002: * Copyright 2002-2007 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: * http://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:
017: package org.springframework.orm.toplink;
018:
019: import java.lang.reflect.Constructor;
020: import java.lang.reflect.Method;
021: import java.util.HashMap;
022: import java.util.Map;
023: import java.util.Properties;
024:
025: import javax.sql.DataSource;
026:
027: import oracle.toplink.exceptions.TopLinkException;
028: import oracle.toplink.internal.databaseaccess.DatabasePlatform;
029: import oracle.toplink.jndi.JNDIConnector;
030: import oracle.toplink.sessionbroker.SessionBroker;
031: import oracle.toplink.sessions.DatabaseLogin;
032: import oracle.toplink.sessions.DatabaseSession;
033: import oracle.toplink.sessions.SessionLog;
034: import oracle.toplink.threetier.ServerSession;
035: import oracle.toplink.tools.sessionconfiguration.XMLLoader;
036: import oracle.toplink.tools.sessionmanagement.SessionManager;
037: import org.apache.commons.logging.Log;
038: import org.apache.commons.logging.LogFactory;
039:
040: import org.springframework.beans.BeanWrapperImpl;
041: import org.springframework.util.ClassUtils;
042: import org.springframework.util.CollectionUtils;
043: import org.springframework.util.ReflectionUtils;
044:
045: /**
046: * Convenient JavaBean-style factory for a TopLink SessionFactory instance.
047: * Loads a TopLink <code>sessions.xml</code> file from the class path, exposing a
048: * specific TopLink Session defined there (usually a ServerSession).
049: *
050: * <p>TopLink Session configuration is done using a <code>sessions.xml</code> file.
051: * The most convenient way to create the <code>sessions.xml</code> file is to use
052: * the Oracle TopLink SessionsEditor workbench. The <code>sessions.xml</code> file
053: * contains all runtime configuration and points to a second XML or Class resource
054: * from which to load the actual TopLink project metadata (which defines mappings).
055: *
056: * <p>LocalSessionFactory loads the <code>sessions.xml</code> file during
057: * initialization in order to bootstrap the specified TopLink (Server)Session.
058: * The name of the actual config resource and the name of the Session to be loaded,
059: * if different from <code>sessions.xml</code> and "Session", respectively, can be
060: * configured through bean properties.
061: *
062: * <p>All resources (<code>sessions.xml</code> and Mapping Workbench metadata) are
063: * loaded using <code>ClassLoader.getResourceAsStream</code> calls by TopLink, so
064: * users may need to configure a ClassLoader with appropriate visibility. This is
065: * particularly important in J2EE environments where the TopLink metadata might be
066: * deployed to a different location than the Spring configuration. The ClassLoader
067: * used to search for the TopLink metadata and to load the persistent classes
068: * defined there will default to the the context ClassLoader for the current Thread.
069: *
070: * <p>TopLink's debug logging can be redirected to Commons Logging by passing a
071: * CommonsLoggingSessionLog to the "sessionLog" bean property. Otherwise, TopLink
072: * uses it's own DefaultSessionLog, whose levels are configured in the
073: * <code>sessions.xml</code> file.
074: *
075: * <p>This class has been tested against both TopLink 9.0.4 and TopLink 10.1.3.
076: * It will automatically adapt to the TopLink version encountered: for example,
077: * using an XMLSessionConfigLoader on 10.1.3, but an XMLLoader on 9.0.4.
078: *
079: * <p><b>NOTE:</b> When defining a TopLink SessionFactory in a Spring application
080: * context, you will usually define a bean of type <b>LocalSessionFactoryBean</b>.
081: * LocalSessionFactoryBean is a subclass of this factory, which will automatically
082: * expose the created TopLink SessionFactory instance as bean reference.
083: *
084: * @author Juergen Hoeller
085: * @author <a href="mailto:james.x.clark@oracle.com">James Clark</a>
086: * @since 1.2
087: * @see LocalSessionFactoryBean
088: * @see TopLinkTemplate#setSessionFactory
089: * @see TopLinkTransactionManager#setSessionFactory
090: * @see SingleSessionFactory
091: * @see ServerSessionFactory
092: * @see oracle.toplink.threetier.ServerSession
093: * @see oracle.toplink.tools.sessionconfiguration.XMLLoader
094: * @see oracle.toplink.tools.sessionconfiguration.XMLSessionConfigLoader
095: */
096: public class LocalSessionFactory {
097:
098: /**
099: * The default location of the <code>sessions.xml</code> TopLink configuration file:
100: * "sessions.xml" in the class path.
101: */
102: public static final String DEFAULT_SESSIONS_XML = "sessions.xml";
103:
104: /**
105: * The default session name to look for in the sessions.xml: "Session".
106: */
107: public static final String DEFAULT_SESSION_NAME = "Session";
108:
109: protected final Log logger = LogFactory.getLog(getClass());
110:
111: /**
112: * The classpath location of the sessions TopLink configuration file.
113: */
114: private String configLocation = DEFAULT_SESSIONS_XML;
115:
116: /**
117: * The session name to look for in the sessions.xml configuration file.
118: */
119: private String sessionName = DEFAULT_SESSION_NAME;
120:
121: /**
122: * The ClassLoader to use to load the sessions.xml and project XML files.
123: */
124: private ClassLoader sessionClassLoader;
125:
126: private DatabaseLogin databaseLogin;
127:
128: private final Map loginPropertyMap = new HashMap();
129:
130: private DataSource dataSource;
131:
132: private DatabasePlatform databasePlatform;
133:
134: private SessionLog sessionLog;
135:
136: /**
137: * Set the TopLink <code>sessions.xml</code> configuration file that defines
138: * TopLink Sessions, as class path resource location.
139: * <p>The <code>sessions.xml</code> file will usually be placed in the META-INF
140: * directory or root path of a JAR file, or the <code>WEB-INF/classes</code>
141: * directory of a WAR file (specifying "META-INF/toplink-sessions.xml" or
142: * simply "toplink-sessions.xml" as config location, respectively).
143: * <p>The default config location is "sessions.xml" in the root of the class path.
144: * @param configLocation the class path location of the <code>sessions.xml</code> file
145: */
146: public void setConfigLocation(String configLocation) {
147: this .configLocation = configLocation;
148: }
149:
150: /**
151: * Set the name of the TopLink Session, as defined in TopLink's
152: * <code>sessions.xml</code> configuration file.
153: * The default session name is "Session".
154: */
155: public void setSessionName(String sessionName) {
156: this .sessionName = sessionName;
157: }
158:
159: /**
160: * Set the ClassLoader that should be used to lookup the config resources.
161: * If nothing is set here, then we will try to use the Thread context ClassLoader
162: * and the ClassLoader that loaded this factory class, in that order.
163: * <p>This ClassLoader will be used to load the TopLink configuration files
164: * and the project metadata. Furthermore, the TopLink ConversionManager will
165: * use this ClassLoader to load all TopLink entity classes. If the latter is not
166: * appropriate, users can configure a pre-login SessionEvent to alter the
167: * ConversionManager ClassLoader that TopLink will use at runtime.
168: */
169: public void setSessionClassLoader(ClassLoader sessionClassLoader) {
170: this .sessionClassLoader = sessionClassLoader;
171: }
172:
173: /**
174: * Specify the DatabaseLogin instance that carries the TopLink database
175: * configuration to use. This is an alternative to specifying that information
176: * in a <login> tag in the <code>sessions.xml</code> configuration file,
177: * allowing for configuring a DatabaseLogin instance as standard Spring bean
178: * definition (being able to leverage Spring's placeholder mechanism, etc).
179: * <p>The DatabaseLogin instance can either carry traditional JDBC config properties
180: * or hold a nested TopLink Connector instance, pointing to the connection pool to use.
181: * DatabaseLogin also holds the TopLink DatabasePlatform instance that defines the
182: * database product that TopLink is talking to (for example, HSQLPlatform).
183: * <p><b>WARNING:</b> Overriding the Login instance has been reported to not
184: * work on TopLink 10.1.3.0 and 10.1.3.1. Specify {@link #setLoginProperties
185: * "loginProperties"} or {@link #getLoginPropertyMap "loginPropertyMap[...]"}
186: * entries instead, if you prefer to have the login configuration defined
187: * on the Spring LocalSessionFactory.
188: */
189: public void setDatabaseLogin(DatabaseLogin databaseLogin) {
190: this .databaseLogin = databaseLogin;
191: }
192:
193: /**
194: * Specify TopLink login properties, to be passed to
195: * the {@link oracle.toplink.sessions.DatabaseLogin} instance.
196: * <p>Can be populated with a String "value" (parsed via PropertiesEditor)
197: * or a "props" element in XML bean definitions.
198: * @see oracle.toplink.sessions.DatabaseLogin
199: */
200: public void setLoginProperties(Properties loginProperties) {
201: CollectionUtils.mergePropertiesIntoMap(loginProperties,
202: this .loginPropertyMap);
203: }
204:
205: /**
206: * Specify TopLink login properties as a Map, to be passed to
207: * the {@link oracle.toplink.sessions.DatabaseLogin} instance.
208: * <p>Can be populated with a "map" or "props" element in XML bean definitions.
209: * @see oracle.toplink.sessions.DatabaseLogin
210: */
211: public void setLoginPropertyMap(Map loginProperties) {
212: if (loginProperties != null) {
213: this .loginPropertyMap.putAll(loginProperties);
214: }
215: }
216:
217: /**
218: * Allow Map access to the TopLink login properties to be passed to the
219: * DatabaseLogin instance, with the option to add or override specific entries.
220: * <p>Useful for specifying entries directly, for example via
221: * "loginPropertyMap[tableQualifier]".
222: * @see oracle.toplink.sessions.DatabaseLogin
223: */
224: public Map getLoginPropertyMap() {
225: return this .loginPropertyMap;
226: }
227:
228: /**
229: * Specify a standard JDBC DataSource that TopLink should use as connection pool.
230: * This allows for using a shared DataSource definition instead of TopLink's
231: * own connection pool.
232: * <p>A passed-in DataSource will be wrapped in an appropriate TopLink Connector
233: * and registered with the TopLink DatabaseLogin instance (either the default
234: * instance or one passed in through the "databaseLogin" property). The
235: * "usesExternalConnectionPooling" flag will automatically be set to "true".
236: * @see oracle.toplink.sessions.DatabaseLogin#setConnector(oracle.toplink.sessions.Connector)
237: * @see oracle.toplink.sessions.DatabaseLogin#setUsesExternalConnectionPooling(boolean)
238: * @see #setDatabaseLogin(oracle.toplink.sessions.DatabaseLogin)
239: */
240: public void setDataSource(DataSource dataSource) {
241: this .dataSource = dataSource;
242: }
243:
244: /**
245: * Specify the TopLink DatabasePlatform instance that the Session should use:
246: * for example, HSQLPlatform. This is an alternative to specifying the platform
247: * in a <login> tag in the <code>sessions.xml</code> configuration file.
248: * <p>A passed-in DatabasePlatform will be registered with the TopLink
249: * DatabaseLogin instance (either the default instance or one passed in
250: * through the "databaseLogin" property).
251: * @see oracle.toplink.internal.databaseaccess.HSQLPlatform
252: * @see oracle.toplink.platform.database.HSQLPlatform
253: */
254: public void setDatabasePlatform(DatabasePlatform databasePlatform) {
255: this .databasePlatform = databasePlatform;
256: }
257:
258: /**
259: * Specify a TopLink SessionLog instance to use for detailed logging of the
260: * Session's activities: for example, DefaultSessionLog (which logs to the
261: * console), JavaLog (which logs through JDK 1.4'S <code>java.util.logging</code>,
262: * available as of TopLink 10.1.3), or CommonsLoggingSessionLog /
263: * CommonsLoggingSessionLog904 (which logs through Commons Logging,
264: * on TopLink 10.1.3 and 9.0.4, respectively).
265: * <p>Note that detailed Session logging is usually only useful for debug
266: * logging, with adjustable detail level. As of TopLink 10.1.3, TopLink also
267: * uses different log categories, which allows for fine-grained filtering of
268: * log messages. For standard execution, no SessionLog needs to be specified.
269: * @see oracle.toplink.sessions.DefaultSessionLog
270: * @see oracle.toplink.logging.DefaultSessionLog
271: * @see oracle.toplink.logging.JavaLog
272: * @see org.springframework.orm.toplink.support.CommonsLoggingSessionLog
273: * @see org.springframework.orm.toplink.support.CommonsLoggingSessionLog904
274: */
275: public void setSessionLog(SessionLog sessionLog) {
276: this .sessionLog = sessionLog;
277: }
278:
279: /**
280: * Create a TopLink SessionFactory according to the configuration settings.
281: * @return the new TopLink SessionFactory
282: * @throws TopLinkException in case of errors
283: */
284: public SessionFactory createSessionFactory()
285: throws TopLinkException {
286: if (logger.isInfoEnabled()) {
287: logger.info("Initializing TopLink SessionFactory from ["
288: + this .configLocation + "]");
289: }
290:
291: // Determine class loader to use.
292: ClassLoader classLoader = (this .sessionClassLoader != null ? this .sessionClassLoader
293: : ClassUtils.getDefaultClassLoader());
294:
295: // Initialize the TopLink Session, using the configuration file
296: // and the session name.
297: DatabaseSession session = loadDatabaseSession(
298: this .configLocation, this .sessionName, classLoader);
299:
300: // It is possible for SessionManager to return a null Session!
301: if (session == null) {
302: throw new IllegalStateException(
303: "A session named '"
304: + this .sessionName
305: + "' could not be loaded from resource ["
306: + this .configLocation
307: + "] using ClassLoader ["
308: + classLoader
309: + "]. "
310: + "This is most likely a deployment issue: Can the class loader access the resource?");
311: }
312:
313: DatabaseLogin login = (this .databaseLogin != null ? this .databaseLogin
314: : session.getLogin());
315:
316: // Apply specified login properties to the DatabaseLogin instance.
317: if (this .loginPropertyMap != null) {
318: new BeanWrapperImpl(login)
319: .setPropertyValues(this .loginPropertyMap);
320: }
321:
322: // Override default connection pool with specified DataSource, if any.
323: if (this .dataSource != null) {
324: login.setConnector(new JNDIConnector(this .dataSource));
325: login.setUsesExternalConnectionPooling(true);
326: }
327:
328: // Override default DatabasePlatform with specified one, if any.
329: if (this .databasePlatform != null) {
330: login.usePlatform(this .databasePlatform);
331: }
332:
333: // Override default DatabaseLogin instance with specified one, if any.
334: if (this .databaseLogin != null) {
335: setDatabaseLogin(session, this .databaseLogin);
336: }
337:
338: // Override default SessionLog with specified one, if any.
339: if (this .sessionLog != null) {
340: session.setSessionLog(this .sessionLog);
341: session.logMessages();
342: }
343:
344: // Log in and create corresponding SessionFactory.
345: session.login();
346: return newSessionFactory(session);
347: }
348:
349: /**
350: * Handle differences between the <code>Session.setLogin</code> interface
351: * between TopLink 9.0.4 to 10.1.3.
352: * <p>The Login interface was introduced in TopLink 10.1.3.
353: * @param session the DatabaseSession being logged in
354: * @param login the DatabaseLogin injected by Spring
355: * @see oracle.toplink.sessions.DatabaseSession#setLogin
356: */
357: protected void setDatabaseLogin(DatabaseSession session,
358: DatabaseLogin login) {
359: Method setLoginMethod = null;
360: try {
361: // Search for the new 10.1.3 Login interface...
362: Class loginClass = Class
363: .forName("oracle.toplink.sessions.Login");
364: setLoginMethod = DatabaseSession.class.getMethod(
365: "setLogin", new Class[] { loginClass });
366: if (logger.isDebugEnabled()) {
367: logger
368: .debug("Using TopLink 10.1.3 setLogin(Login) API");
369: }
370: } catch (Exception ex) {
371: // TopLink 10.1.3 Login interface not found ->
372: // fall back to TopLink 9.0.4's setLogin(DatabaseLogin)
373: if (logger.isDebugEnabled()) {
374: logger
375: .debug("Using TopLink 9.0.4 setLogin(DatabaseLogin) API");
376: }
377: session.setLogin(login);
378: return;
379: }
380:
381: // Invoke the 10.1.3 version: Session.setLogin(Login)
382: ReflectionUtils.invokeMethod(setLoginMethod, session,
383: new Object[] { login });
384: }
385:
386: /**
387: * Load the specified DatabaseSession from the TopLink <code>sessions.xml</code>
388: * configuration file.
389: * @param configLocation the class path location of the <code>sessions.xml</code> file
390: * @param sessionName the name of the TopLink Session in the configuration file
391: * @param sessionClassLoader the class loader to use
392: * @return the DatabaseSession instance
393: * @throws TopLinkException in case of errors
394: */
395: protected DatabaseSession loadDatabaseSession(
396: String configLocation, String sessionName,
397: ClassLoader sessionClassLoader) throws TopLinkException {
398:
399: SessionManager manager = getSessionManager();
400:
401: // Try to find TopLink 10.1.3 XMLSessionConfigLoader.
402: Method getSessionMethod = null;
403: Object loader = null;
404: try {
405: Class loaderClass = Class
406: .forName("oracle.toplink.tools.sessionconfiguration.XMLSessionConfigLoader");
407: getSessionMethod = SessionManager.class.getMethod(
408: "getSession",
409: new Class[] { loaderClass, String.class,
410: ClassLoader.class, boolean.class,
411: boolean.class, boolean.class });
412: if (logger.isDebugEnabled()) {
413: logger
414: .debug("Using TopLink 10.1.3 XMLSessionConfigLoader");
415: }
416: Constructor ctor = loaderClass
417: .getConstructor(new Class[] { String.class });
418: loader = ctor.newInstance(new Object[] { configLocation });
419: } catch (Exception ex) {
420: // TopLink 10.1.3 XMLSessionConfigLoader not found ->
421: // fall back to TopLink 9.0.4 XMLLoader.
422: if (logger.isDebugEnabled()) {
423: logger.debug("Using TopLink 9.0.4 XMLLoader");
424: }
425: XMLLoader xmlLoader = new XMLLoader(configLocation);
426: return (DatabaseSession) manager.getSession(xmlLoader,
427: sessionName, sessionClassLoader, false, false);
428: }
429:
430: // TopLink 10.1.3 XMLSessionConfigLoader found -> create loader instance
431: // through reflection and fetch specified Session from SessionManager.
432: // This invocation will check if the ClassLoader passed in is the same
433: // as the one used to a session currently loaded with the same "sessionName"
434: // If the ClassLoaders are different, then this LocalSessionFactory is being
435: // re-loaded after a hot-deploy and the existing DatabaseSession will be logged
436: // out and re-built from scratch.
437: return (DatabaseSession) ReflectionUtils.invokeMethod(
438: getSessionMethod, manager, new Object[] { loader,
439: sessionName, sessionClassLoader, Boolean.FALSE,
440: Boolean.FALSE, Boolean.TRUE });
441: }
442:
443: /**
444: * Return the TopLink SessionManager to use for loading DatabaseSessions.
445: * <p>The default implementation creates a new plain SessionManager instance,
446: * leading to completely independent TopLink Session instances. Could be
447: * overridden to return a shared or pre-configured SessionManager.
448: * @return the TopLink SessionManager instance
449: */
450: protected SessionManager getSessionManager() {
451: return new SessionManager();
452: }
453:
454: /**
455: * Create a new SessionFactory for the given TopLink DatabaseSession.
456: * <p>The default implementation creates a ServerSessionFactory for a
457: * ServerSession and a SingleSessionFactory for a plain DatabaseSession.
458: * @param session the TopLink DatabaseSession to create a SessionFactory for
459: * @return the SessionFactory
460: * @throws TopLinkException in case of errors
461: * @see ServerSessionFactory
462: * @see SingleSessionFactory
463: * @see oracle.toplink.threetier.ServerSession
464: * @see oracle.toplink.sessions.DatabaseSession
465: */
466: protected SessionFactory newSessionFactory(DatabaseSession session) {
467: if (session instanceof ServerSession) {
468: return new ServerSessionFactory((ServerSession) session);
469: } else if (session instanceof SessionBroker) {
470: return new SessionBrokerSessionFactory(
471: (SessionBroker) session);
472: } else {
473: return new SingleSessionFactory(session);
474: }
475: }
476:
477: }
|