001    /*
002     * JBoss DNA (http://www.jboss.org/dna)
003     * See the COPYRIGHT.txt file distributed with this work for information
004     * regarding copyright ownership.  Some portions may be licensed
005     * to Red Hat, Inc. under one or more contributor license agreements.
006     * See the AUTHORS.txt file in the distribution for a full listing of 
007     * individual contributors. 
008     *
009     * JBoss DNA is free software. Unless otherwise indicated, all code in JBoss DNA
010     * is licensed to you under the terms of the GNU Lesser General Public License as
011     * published by the Free Software Foundation; either version 2.1 of
012     * the License, or (at your option) any later version.
013     *
014     * JBoss DNA is distributed in the hope that it will be useful,
015     * but WITHOUT ANY WARRANTY; without even the implied warranty of
016     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
017     * Lesser General Public License for more details.
018     *
019     * You should have received a copy of the GNU Lesser General Public
020     * License along with this software; if not, write to the Free
021     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
022     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
023     */
024    package org.jboss.dna.connector.store.jpa;
025    
026    import java.util.ArrayList;
027    import java.util.Arrays;
028    import java.util.Collection;
029    import java.util.Collections;
030    import java.util.Enumeration;
031    import java.util.HashMap;
032    import java.util.Hashtable;
033    import java.util.List;
034    import java.util.Map;
035    import java.util.UUID;
036    import javax.naming.Context;
037    import javax.naming.InitialContext;
038    import javax.naming.RefAddr;
039    import javax.naming.Reference;
040    import javax.naming.StringRefAddr;
041    import javax.naming.spi.ObjectFactory;
042    import javax.persistence.EntityManager;
043    import javax.persistence.EntityManagerFactory;
044    import javax.sql.DataSource;
045    import net.jcip.annotations.Immutable;
046    import org.hibernate.ejb.Ejb3Configuration;
047    import org.jboss.dna.common.i18n.I18n;
048    import org.jboss.dna.common.util.CheckArg;
049    import org.jboss.dna.common.util.Logger;
050    import org.jboss.dna.common.util.StringUtil;
051    import org.jboss.dna.connector.store.jpa.model.basic.BasicModel;
052    import org.jboss.dna.connector.store.jpa.util.StoreOptionEntity;
053    import org.jboss.dna.connector.store.jpa.util.StoreOptions;
054    import org.jboss.dna.graph.ExecutionContext;
055    import org.jboss.dna.graph.cache.CachePolicy;
056    import org.jboss.dna.graph.connector.RepositoryConnection;
057    import org.jboss.dna.graph.connector.RepositoryContext;
058    import org.jboss.dna.graph.connector.RepositorySource;
059    import org.jboss.dna.graph.connector.RepositorySourceCapabilities;
060    import org.jboss.dna.graph.connector.RepositorySourceException;
061    
062    /**
063     * The {@link RepositorySource} for the connector that stores content in a (custom) relational database. This connector uses Java
064     * Persistence API as the interface to the database, with Hibernate as the JPA implementation. (Note that some Hibernate-specific
065     * features are used.)
066     * 
067     * @author Randall Hauch
068     */
069    public class JpaSource implements RepositorySource, ObjectFactory {
070    
071        /**
072         * This source is capable of using different database schemas
073         * 
074         * @author Randall Hauch
075         */
076        public static class Models {
077            public static final Model BASIC = new BasicModel();
078            private static final Model[] ALL_ARRAY = new Model[] {BASIC};
079            private static final List<Model> MODIFIABLE_MODELS = new ArrayList<Model>(Arrays.asList(ALL_ARRAY));
080            public static final Collection<Model> ALL = Collections.unmodifiableCollection(MODIFIABLE_MODELS);
081            public static final Model DEFAULT = BASIC;
082    
083            public static boolean addModel( Model model ) {
084                CheckArg.isNotNull(model, "modelName");
085                for (Model existing : MODIFIABLE_MODELS) {
086                    if (existing.getName().equals(model.getName())) return false;
087                }
088                return MODIFIABLE_MODELS.add(model);
089            }
090    
091            public static Model getModel( String name ) {
092                CheckArg.isNotEmpty(name, "name");
093                name = name.trim();
094                for (Model existing : ALL) {
095                    if (existing.getName().equals(name)) return existing;
096                }
097                return null;
098            }
099        }
100    
101        protected static final String SOURCE_NAME = "sourceName";
102        protected static final String ROOT_NODE_UUID = "rootNodeUuid";
103        protected static final String DATA_SOURCE_JNDI_NAME = "dataSourceJndiName";
104        protected static final String DIALECT = "dialect";
105        protected static final String USERNAME = "username";
106        protected static final String PASSWORD = "password";
107        protected static final String URL = "url";
108        protected static final String DRIVER_CLASS_NAME = "driverClassName";
109        protected static final String DRIVER_CLASSLOADER_NAME = "driverClassloaderName";
110        protected static final String MAXIMUM_CONNECTIONS_IN_POOL = "maximumConnectionsInPool";
111        protected static final String MINIMUM_CONNECTIONS_IN_POOL = "minimumConnectionsInPool";
112        protected static final String MAXIMUM_CONNECTION_IDLE_TIME_IN_SECONDS = "maximumConnectionIdleTimeInSeconds";
113        protected static final String MAXIMUM_SIZE_OF_STATEMENT_CACHE = "maximumSizeOfStatementCache";
114        protected static final String NUMBER_OF_CONNECTIONS_TO_BE_ACQUIRED_AS_NEEDED = "numberOfConnectionsToBeAcquiredAsNeeded";
115        protected static final String IDLE_TIME_IN_SECONDS_BEFORE_TESTING_CONNECTIONS = "idleTimeInSecondsBeforeTestingConnections";
116        protected static final String CACHE_TIME_TO_LIVE_IN_MILLISECONDS = "cacheTimeToLiveInMilliseconds";
117        protected static final String RETRY_LIMIT = "retryLimit";
118        protected static final String MODEL_NAME = "modelName";
119        protected static final String LARGE_VALUE_SIZE_IN_BYTES = "largeValueSizeInBytes";
120        protected static final String COMPRESS_DATA = "compressData";
121        protected static final String ENFORCE_REFERENTIAL_INTEGRITY = "enforceReferentialIntegrity";
122        protected static final String DEFAULT_WORKSPACE = "defaultWorkspace";
123        protected static final String PREDEFINED_WORKSPACE_NAMES = "predefinedWorkspaceNames";
124        protected static final String ALLOW_CREATING_WORKSPACES = "allowCreatingWorkspaces";
125    
126        /**
127         * This source supports events.
128         */
129        protected static final boolean SUPPORTS_EVENTS = true;
130        /**
131         * This source supports same-name-siblings.
132         */
133        protected static final boolean SUPPORTS_SAME_NAME_SIBLINGS = true;
134        /**
135         * This source supports creating references.
136         */
137        protected static final boolean SUPPORTS_REFERENCES = true;
138        /**
139         * This source supports udpates by default, but each instance may be configured to {@link #setSupportsUpdates(boolean) be
140         * read-only or updateable}.
141         */
142        public static final boolean DEFAULT_SUPPORTS_UPDATES = true;
143        /**
144         * This source does support creating workspaces.
145         */
146        public static final boolean DEFAULT_SUPPORTS_CREATING_WORKSPACES = true;
147    
148        /**
149         * The default UUID that is used for root nodes in a store.
150         */
151        public static final String DEFAULT_ROOT_NODE_UUID = "1497b6fe-8c7e-4bbb-aaa2-24f3d4942668";
152    
153        /**
154         * The initial {@link #getNameOfDefaultWorkspace() name of the default workspace} is "{@value} ", unless otherwise specified.
155         */
156        public static final String DEFAULT_NAME_OF_DEFAULT_WORKSPACE = "default";
157    
158        private static final int DEFAULT_RETRY_LIMIT = 0;
159        private static final int DEFAULT_CACHE_TIME_TO_LIVE_IN_SECONDS = 60 * 5; // 5 minutes
160        private static final int DEFAULT_MAXIMUM_FETCH_DEPTH = 3;
161        private static final int DEFAULT_MAXIMUM_CONNECTIONS_IN_POOL = 5;
162        private static final int DEFAULT_MINIMUM_CONNECTIONS_IN_POOL = 0;
163        private static final int DEFAULT_MAXIMUM_CONNECTION_IDLE_TIME_IN_SECONDS = 60 * 10; // 10 minutes
164        private static final int DEFAULT_MAXIMUM_NUMBER_OF_STATEMENTS_TO_CACHE = 100;
165        private static final int DEFAULT_NUMBER_OF_CONNECTIONS_TO_ACQUIRE_AS_NEEDED = 1;
166        private static final int DEFAULT_IDLE_TIME_IN_SECONDS_BEFORE_TESTING_CONNECTIONS = 60 * 3; // 3 minutes
167        private static final int DEFAULT_LARGE_VALUE_SIZE_IN_BYTES = 2 ^ 10; // 1 kilobyte
168        private static final boolean DEFAULT_COMPRESS_DATA = true;
169        private static final boolean DEFAULT_ENFORCE_REFERENTIAL_INTEGRITY = true;
170    
171        /**
172         * The first serialized version of this source.
173         */
174        private static final long serialVersionUID = 1L;
175    
176        private volatile String name;
177        private volatile String dataSourceJndiName;
178        private volatile String dialect;
179        private volatile String username;
180        private volatile String password;
181        private volatile String url;
182        private volatile String driverClassName;
183        private volatile String driverClassloaderName;
184        private volatile String rootNodeUuid = DEFAULT_ROOT_NODE_UUID;
185        private volatile int maximumConnectionsInPool = DEFAULT_MAXIMUM_CONNECTIONS_IN_POOL;
186        private volatile int minimumConnectionsInPool = DEFAULT_MINIMUM_CONNECTIONS_IN_POOL;
187        private volatile int maximumConnectionIdleTimeInSeconds = DEFAULT_MAXIMUM_CONNECTION_IDLE_TIME_IN_SECONDS;
188        private volatile int maximumSizeOfStatementCache = DEFAULT_MAXIMUM_NUMBER_OF_STATEMENTS_TO_CACHE;
189        private volatile int numberOfConnectionsToAcquireAsNeeded = DEFAULT_NUMBER_OF_CONNECTIONS_TO_ACQUIRE_AS_NEEDED;
190        private volatile int idleTimeInSecondsBeforeTestingConnections = DEFAULT_IDLE_TIME_IN_SECONDS_BEFORE_TESTING_CONNECTIONS;
191        private volatile int retryLimit = DEFAULT_RETRY_LIMIT;
192        private volatile int cacheTimeToLiveInMilliseconds = DEFAULT_CACHE_TIME_TO_LIVE_IN_SECONDS * 1000;
193        private volatile long largeValueSizeInBytes = DEFAULT_LARGE_VALUE_SIZE_IN_BYTES;
194        private volatile boolean compressData = DEFAULT_COMPRESS_DATA;
195        private volatile boolean referentialIntegrityEnforced = DEFAULT_ENFORCE_REFERENTIAL_INTEGRITY;
196        private volatile String defaultWorkspace = DEFAULT_NAME_OF_DEFAULT_WORKSPACE;
197        private volatile String[] predefinedWorkspaces = new String[] {};
198        private volatile RepositorySourceCapabilities capabilities = new RepositorySourceCapabilities(
199                                                                                                      SUPPORTS_SAME_NAME_SIBLINGS,
200                                                                                                      DEFAULT_SUPPORTS_UPDATES,
201                                                                                                      SUPPORTS_EVENTS,
202                                                                                                      DEFAULT_SUPPORTS_CREATING_WORKSPACES,
203                                                                                                      SUPPORTS_REFERENCES);
204        private volatile String modelName;
205        private transient Model model;
206        private transient DataSource dataSource;
207        private transient EntityManagerFactory entityManagerFactory;
208        private transient CachePolicy cachePolicy;
209        private transient RepositoryContext repositoryContext;
210        private transient UUID rootUuid = UUID.fromString(rootNodeUuid);
211    
212        /**
213         * {@inheritDoc}
214         * 
215         * @see org.jboss.dna.graph.connector.RepositorySource#getName()
216         */
217        public String getName() {
218            return name;
219        }
220    
221        /**
222         * Set the name for the source
223         * 
224         * @param name the new name for the source
225         */
226        public void setName( String name ) {
227            if (name != null) {
228                name = name.trim();
229                if (name.length() == 0) name = null;
230            }
231            this.name = name;
232        }
233    
234        /**
235         * {@inheritDoc}
236         * 
237         * @see org.jboss.dna.graph.connector.RepositorySource#getCapabilities()
238         */
239        public RepositorySourceCapabilities getCapabilities() {
240            return capabilities;
241        }
242    
243        /**
244         * Get whether this source supports updates.
245         * 
246         * @return true if this source supports updates, or false if this source only supports reading content.
247         */
248        public boolean getSupportsUpdates() {
249            return capabilities.supportsUpdates();
250        }
251    
252        /**
253         * Set whether this source supports updates.
254         * 
255         * @param supportsUpdates true if this source supports updating content, or false if this source only supports reading
256         *        content.
257         */
258        public synchronized void setSupportsUpdates( boolean supportsUpdates ) {
259            capabilities = new RepositorySourceCapabilities(capabilities.supportsSameNameSiblings(), supportsUpdates,
260                                                            capabilities.supportsEvents(), capabilities.supportsCreatingWorkspaces(),
261                                                            capabilities.supportsReferences());
262        }
263    
264        /**
265         * {@inheritDoc}
266         * 
267         * @see org.jboss.dna.graph.connector.RepositorySource#getRetryLimit()
268         */
269        public int getRetryLimit() {
270            return retryLimit;
271        }
272    
273        /**
274         * {@inheritDoc}
275         * 
276         * @see org.jboss.dna.graph.connector.RepositorySource#setRetryLimit(int)
277         */
278        public synchronized void setRetryLimit( int limit ) {
279            if (limit < 0) limit = 0;
280            this.retryLimit = limit;
281        }
282    
283        /**
284         * Get the time in milliseconds that content returned from this source may used while in the cache.
285         * 
286         * @return the time to live, in milliseconds, or 0 if the time to live is not specified by this source
287         */
288        public int getCacheTimeToLiveInMilliseconds() {
289            return cacheTimeToLiveInMilliseconds;
290        }
291    
292        /**
293         * Set the time in milliseconds that content returned from this source may used while in the cache.
294         * 
295         * @param cacheTimeToLive the time to live, in milliseconds; 0 if the time to live is not specified by this source; or a
296         *        negative number for the default value
297         */
298        public synchronized void setCacheTimeToLiveInMilliseconds( int cacheTimeToLive ) {
299            if (cacheTimeToLive < 0) cacheTimeToLive = DEFAULT_CACHE_TIME_TO_LIVE_IN_SECONDS;
300            this.cacheTimeToLiveInMilliseconds = cacheTimeToLive;
301            this.cachePolicy = cacheTimeToLiveInMilliseconds > 0 ? new JpaCachePolicy(cacheTimeToLiveInMilliseconds) : null;
302        }
303    
304        /**
305         * @return rootNodeUuid
306         */
307        public String getRootNodeUuid() {
308            return rootNodeUuid;
309        }
310    
311        /**
312         * @param rootNodeUuid Sets rootNodeUuid to the specified value.
313         * @throws IllegalArgumentException if the string value cannot be converted to UUID
314         */
315        public void setRootNodeUuid( String rootNodeUuid ) {
316            if (rootNodeUuid != null && rootNodeUuid.trim().length() == 0) rootNodeUuid = DEFAULT_ROOT_NODE_UUID;
317            this.rootUuid = UUID.fromString(rootNodeUuid);
318            this.rootNodeUuid = rootNodeUuid;
319        }
320    
321        /**
322         * Get the name of the default workspace.
323         * 
324         * @return the name of the workspace that should be used by default, or null if there is no default workspace
325         */
326        public String getNameOfDefaultWorkspace() {
327            return defaultWorkspace;
328        }
329    
330        /**
331         * Set the name of the workspace that should be used when clients don't specify a workspace.
332         * 
333         * @param nameOfDefaultWorkspace the name of the workspace that should be used by default, or null if the
334         *        {@link #DEFAULT_NAME_OF_DEFAULT_WORKSPACE default name} should be used
335         */
336        public synchronized void setNameOfDefaultWorkspace( String nameOfDefaultWorkspace ) {
337            this.defaultWorkspace = nameOfDefaultWorkspace != null ? nameOfDefaultWorkspace : DEFAULT_NAME_OF_DEFAULT_WORKSPACE;
338        }
339    
340        /**
341         * Gets the names of the workspaces that are available when this source is created.
342         * 
343         * @return the names of the workspaces that this source starts with, or null if there are no such workspaces
344         * @see #setPredefinedWorkspaceNames(String[])
345         * @see #setCreatingWorkspacesAllowed(boolean)
346         */
347        public synchronized String[] getPredefinedWorkspaceNames() {
348            String[] copy = new String[predefinedWorkspaces.length];
349            System.arraycopy(predefinedWorkspaces, 0, copy, 0, predefinedWorkspaces.length);
350            return copy;
351        }
352    
353        /**
354         * Sets the names of the workspaces that are available when this source is created.
355         * 
356         * @param predefinedWorkspaceNames the names of the workspaces that this source should start with, or null if there are no
357         *        such workspaces
358         * @see #setCreatingWorkspacesAllowed(boolean)
359         * @see #getPredefinedWorkspaceNames()
360         */
361        public synchronized void setPredefinedWorkspaceNames( String[] predefinedWorkspaceNames ) {
362            this.predefinedWorkspaces = predefinedWorkspaceNames != null ? predefinedWorkspaceNames : new String[] {};
363        }
364    
365        /**
366         * Get whether this source allows workspaces to be created dynamically.
367         * 
368         * @return true if this source allows workspaces to be created by clients, or false if the
369         *         {@link #getPredefinedWorkspaceNames() set of workspaces} is fixed
370         * @see #setPredefinedWorkspaceNames(String[])
371         * @see #getPredefinedWorkspaceNames()
372         * @see #setCreatingWorkspacesAllowed(boolean)
373         */
374        public boolean isCreatingWorkspacesAllowed() {
375            return capabilities.supportsCreatingWorkspaces();
376        }
377    
378        /**
379         * Set whether this source allows workspaces to be created dynamically.
380         * 
381         * @param allowWorkspaceCreation true if this source allows workspaces to be created by clients, or false if the
382         *        {@link #getPredefinedWorkspaceNames() set of workspaces} is fixed
383         * @see #setPredefinedWorkspaceNames(String[])
384         * @see #getPredefinedWorkspaceNames()
385         * @see #isCreatingWorkspacesAllowed()
386         */
387        public synchronized void setCreatingWorkspacesAllowed( boolean allowWorkspaceCreation ) {
388            capabilities = new RepositorySourceCapabilities(capabilities.supportsSameNameSiblings(), capabilities.supportsUpdates(),
389                                                            capabilities.supportsEvents(), allowWorkspaceCreation,
390                                                            capabilities.supportsReferences());
391        }
392    
393        /**
394         * @return dialect
395         */
396        public String getDialect() {
397            return dialect;
398        }
399    
400        /**
401         * @param dialect Sets dialect to the specified value.
402         */
403        public synchronized void setDialect( String dialect ) {
404            if (dialect != null && dialect.trim().length() == 0) dialect = null;
405            this.dialect = dialect;
406        }
407    
408        /**
409         * @return dataSourceJndiName
410         */
411        public String getDataSourceJndiName() {
412            return dataSourceJndiName;
413        }
414    
415        /**
416         * @param dataSourceJndiName Sets dataSourceJndiName to the specified value.
417         */
418        public void setDataSourceJndiName( String dataSourceJndiName ) {
419            if (dataSourceJndiName != null && dataSourceJndiName.trim().length() == 0) dataSourceJndiName = null;
420            this.dataSourceJndiName = dataSourceJndiName;
421        }
422    
423        /**
424         * @return driverClassName
425         */
426        public String getDriverClassName() {
427            return driverClassName;
428        }
429    
430        /**
431         * @param driverClassName Sets driverClassName to the specified value.
432         */
433        public synchronized void setDriverClassName( String driverClassName ) {
434            if (driverClassName != null && driverClassName.trim().length() == 0) driverClassName = null;
435            this.driverClassName = driverClassName;
436        }
437    
438        /**
439         * @return driverClassloaderName
440         */
441        public String getDriverClassloaderName() {
442            return driverClassloaderName;
443        }
444    
445        /**
446         * @param driverClassloaderName Sets driverClassloaderName to the specified value.
447         */
448        public void setDriverClassloaderName( String driverClassloaderName ) {
449            if (driverClassloaderName != null && driverClassloaderName.trim().length() == 0) driverClassloaderName = null;
450            this.driverClassloaderName = driverClassloaderName;
451        }
452    
453        /**
454         * @return username
455         */
456        public String getUsername() {
457            return username;
458        }
459    
460        /**
461         * @param username Sets username to the specified value.
462         */
463        public synchronized void setUsername( String username ) {
464            this.username = username;
465        }
466    
467        /**
468         * @return password
469         */
470        public String getPassword() {
471            return password;
472        }
473    
474        /**
475         * @param password Sets password to the specified value.
476         */
477        public synchronized void setPassword( String password ) {
478            this.password = password;
479        }
480    
481        /**
482         * @return url
483         */
484        public String getUrl() {
485            return url;
486        }
487    
488        /**
489         * @param url Sets url to the specified value.
490         */
491        public synchronized void setUrl( String url ) {
492            if (url != null && url.trim().length() == 0) url = null;
493            this.url = url;
494        }
495    
496        /**
497         * @return maximumConnectionsInPool
498         */
499        public int getMaximumConnectionsInPool() {
500            return maximumConnectionsInPool;
501        }
502    
503        /**
504         * @param maximumConnectionsInPool Sets maximumConnectionsInPool to the specified value.
505         */
506        public synchronized void setMaximumConnectionsInPool( int maximumConnectionsInPool ) {
507            if (maximumConnectionsInPool < 0) maximumConnectionsInPool = DEFAULT_MAXIMUM_CONNECTIONS_IN_POOL;
508            this.maximumConnectionsInPool = maximumConnectionsInPool;
509        }
510    
511        /**
512         * @return minimumConnectionsInPool
513         */
514        public int getMinimumConnectionsInPool() {
515            return minimumConnectionsInPool;
516        }
517    
518        /**
519         * @param minimumConnectionsInPool Sets minimumConnectionsInPool to the specified value.
520         */
521        public synchronized void setMinimumConnectionsInPool( int minimumConnectionsInPool ) {
522            if (minimumConnectionsInPool < 0) minimumConnectionsInPool = DEFAULT_MINIMUM_CONNECTIONS_IN_POOL;
523            this.minimumConnectionsInPool = minimumConnectionsInPool;
524        }
525    
526        /**
527         * @return maximumConnectionIdleTimeInSeconds
528         */
529        public int getMaximumConnectionIdleTimeInSeconds() {
530            return maximumConnectionIdleTimeInSeconds;
531        }
532    
533        /**
534         * @param maximumConnectionIdleTimeInSeconds Sets maximumConnectionIdleTimeInSeconds to the specified value.
535         */
536        public synchronized void setMaximumConnectionIdleTimeInSeconds( int maximumConnectionIdleTimeInSeconds ) {
537            if (maximumConnectionIdleTimeInSeconds < 0) maximumConnectionIdleTimeInSeconds = DEFAULT_MAXIMUM_CONNECTION_IDLE_TIME_IN_SECONDS;
538            this.maximumConnectionIdleTimeInSeconds = maximumConnectionIdleTimeInSeconds;
539        }
540    
541        /**
542         * @return maximumSizeOfStatementCache
543         */
544        public int getMaximumSizeOfStatementCache() {
545            return maximumSizeOfStatementCache;
546        }
547    
548        /**
549         * @param maximumSizeOfStatementCache Sets maximumSizeOfStatementCache to the specified value.
550         */
551        public synchronized void setMaximumSizeOfStatementCache( int maximumSizeOfStatementCache ) {
552            if (maximumSizeOfStatementCache < 0) maximumSizeOfStatementCache = DEFAULT_MAXIMUM_NUMBER_OF_STATEMENTS_TO_CACHE;
553            this.maximumSizeOfStatementCache = maximumSizeOfStatementCache;
554        }
555    
556        /**
557         * @return numberOfConnectionsToAcquireAsNeeded
558         */
559        public int getNumberOfConnectionsToAcquireAsNeeded() {
560            return numberOfConnectionsToAcquireAsNeeded;
561        }
562    
563        /**
564         * @param numberOfConnectionsToAcquireAsNeeded Sets numberOfConnectionsToAcquireAsNeeded to the specified value.
565         */
566        public synchronized void setNumberOfConnectionsToAcquireAsNeeded( int numberOfConnectionsToAcquireAsNeeded ) {
567            if (numberOfConnectionsToAcquireAsNeeded < 0) numberOfConnectionsToAcquireAsNeeded = DEFAULT_NUMBER_OF_CONNECTIONS_TO_ACQUIRE_AS_NEEDED;
568            this.numberOfConnectionsToAcquireAsNeeded = numberOfConnectionsToAcquireAsNeeded;
569        }
570    
571        /**
572         * @return idleTimeInSecondsBeforeTestingConnections
573         */
574        public int getIdleTimeInSecondsBeforeTestingConnections() {
575            return idleTimeInSecondsBeforeTestingConnections;
576        }
577    
578        /**
579         * @param idleTimeInSecondsBeforeTestingConnections Sets idleTimeInSecondsBeforeTestingConnections to the specified value.
580         */
581        public synchronized void setIdleTimeInSecondsBeforeTestingConnections( int idleTimeInSecondsBeforeTestingConnections ) {
582            if (idleTimeInSecondsBeforeTestingConnections < 0) idleTimeInSecondsBeforeTestingConnections = DEFAULT_IDLE_TIME_IN_SECONDS_BEFORE_TESTING_CONNECTIONS;
583            this.idleTimeInSecondsBeforeTestingConnections = idleTimeInSecondsBeforeTestingConnections;
584        }
585    
586        /**
587         * Get the {@link DataSource} object that this source is to use.
588         * 
589         * @return the data source; may be null if no data source has been set or found in JNDI
590         * @see #setDataSource(DataSource)
591         * @see #setDataSourceJndiName(String)
592         */
593        /*package*/DataSource getDataSource() {
594            return dataSource;
595        }
596    
597        /**
598         * Set the {@link DataSource} instance that this source should use.
599         * 
600         * @param dataSource the data source; may be null
601         * @see #getDataSource()
602         * @see #setDataSourceJndiName(String)
603         */
604        /*package*/synchronized void setDataSource( DataSource dataSource ) {
605            this.dataSource = dataSource;
606        }
607    
608        /**
609         * Get the model that will be used. This may be null if not yet connected, but after connections will reflect the type of
610         * model that is being used in the store.
611         * 
612         * @return the name of the model
613         */
614        public String getModel() {
615            return modelName;
616        }
617    
618        /**
619         * Set the model that should be used for this store. If the store already has a model, specifying a different value has no
620         * effect, since the store's model will not be changed. After connection, this value will reflect the actual store value.
621         * 
622         * @param modelName the name of the model that should be used for new stores, or null if the default should be used
623         */
624        public synchronized void setModel( String modelName ) {
625            if (modelName != null) {
626                modelName = modelName.trim();
627                if (modelName.length() == 0) modelName = null;
628            }
629            if (modelName == null) {
630                model = null;
631                return;
632            }
633            Model model = Models.getModel(modelName);
634            if (model == null) {
635                StringBuilder sb = new StringBuilder();
636                boolean first = true;
637                for (Model existing : Models.ALL) {
638                    if (!first) {
639                        first = false;
640                        sb.append(", ");
641                    }
642                    sb.append('"').append(existing.getName()).append('"');
643                }
644                String modelNames = sb.toString();
645                throw new IllegalArgumentException(JpaConnectorI18n.unknownModelName.text(model, modelNames));
646            }
647            this.model = model;
648            this.modelName = modelName;
649        }
650    
651        /**
652         * @return largeValueSizeInBytes
653         */
654        public long getLargeValueSizeInBytes() {
655            return largeValueSizeInBytes;
656        }
657    
658        /**
659         * @param largeValueSizeInBytes Sets largeValueSizeInBytes to the specified value.
660         */
661        public void setLargeValueSizeInBytes( long largeValueSizeInBytes ) {
662            if (largeValueSizeInBytes < 0) largeValueSizeInBytes = DEFAULT_LARGE_VALUE_SIZE_IN_BYTES;
663            this.largeValueSizeInBytes = largeValueSizeInBytes;
664        }
665    
666        /**
667         * @return compressData
668         */
669        public boolean isCompressData() {
670            return compressData;
671        }
672    
673        /**
674         * @param compressData Sets compressData to the specified value.
675         */
676        public void setCompressData( boolean compressData ) {
677            this.compressData = compressData;
678        }
679    
680        /**
681         * @return referentialIntegrityEnforced
682         */
683        public boolean isReferentialIntegrityEnforced() {
684            return referentialIntegrityEnforced;
685        }
686    
687        /**
688         * @param referentialIntegrityEnforced Sets referentialIntegrityEnforced to the specified value.
689         */
690        public void setReferentialIntegrityEnforced( boolean referentialIntegrityEnforced ) {
691            this.referentialIntegrityEnforced = referentialIntegrityEnforced;
692        }
693    
694        /**
695         * {@inheritDoc}
696         * 
697         * @see org.jboss.dna.graph.connector.RepositorySource#initialize(org.jboss.dna.graph.connector.RepositoryContext)
698         */
699        public void initialize( RepositoryContext context ) throws RepositorySourceException {
700            this.repositoryContext = context;
701        }
702    
703        /**
704         * {@inheritDoc}
705         * 
706         * @see javax.naming.Referenceable#getReference()
707         */
708        public Reference getReference() {
709            String className = getClass().getName();
710            String factoryClassName = this.getClass().getName();
711            Reference ref = new Reference(className, factoryClassName, null);
712    
713            ref.add(new StringRefAddr(SOURCE_NAME, getName()));
714            ref.add(new StringRefAddr(ROOT_NODE_UUID, getRootNodeUuid()));
715            ref.add(new StringRefAddr(DATA_SOURCE_JNDI_NAME, getDataSourceJndiName()));
716            ref.add(new StringRefAddr(DIALECT, getDialect()));
717            ref.add(new StringRefAddr(USERNAME, getUsername()));
718            ref.add(new StringRefAddr(PASSWORD, getPassword()));
719            ref.add(new StringRefAddr(URL, getUrl()));
720            ref.add(new StringRefAddr(DRIVER_CLASS_NAME, getDriverClassName()));
721            ref.add(new StringRefAddr(DRIVER_CLASSLOADER_NAME, getDriverClassloaderName()));
722            ref.add(new StringRefAddr(MAXIMUM_CONNECTIONS_IN_POOL, Integer.toString(getMaximumConnectionsInPool())));
723            ref.add(new StringRefAddr(MINIMUM_CONNECTIONS_IN_POOL, Integer.toString(getMinimumConnectionsInPool())));
724            ref.add(new StringRefAddr(MAXIMUM_CONNECTION_IDLE_TIME_IN_SECONDS,
725                                      Integer.toString(getMaximumConnectionIdleTimeInSeconds())));
726            ref.add(new StringRefAddr(MAXIMUM_SIZE_OF_STATEMENT_CACHE, Integer.toString(getMaximumSizeOfStatementCache())));
727            ref.add(new StringRefAddr(NUMBER_OF_CONNECTIONS_TO_BE_ACQUIRED_AS_NEEDED,
728                                      Integer.toString(getNumberOfConnectionsToAcquireAsNeeded())));
729            ref.add(new StringRefAddr(IDLE_TIME_IN_SECONDS_BEFORE_TESTING_CONNECTIONS,
730                                      Integer.toString(getIdleTimeInSecondsBeforeTestingConnections())));
731            ref.add(new StringRefAddr(CACHE_TIME_TO_LIVE_IN_MILLISECONDS, Integer.toString(getCacheTimeToLiveInMilliseconds())));
732            ref.add(new StringRefAddr(LARGE_VALUE_SIZE_IN_BYTES, Long.toString(getLargeValueSizeInBytes())));
733            ref.add(new StringRefAddr(COMPRESS_DATA, Boolean.toString(isCompressData())));
734            ref.add(new StringRefAddr(ENFORCE_REFERENTIAL_INTEGRITY, Boolean.toString(isReferentialIntegrityEnforced())));
735            ref.add(new StringRefAddr(DEFAULT_WORKSPACE, getNameOfDefaultWorkspace()));
736            ref.add(new StringRefAddr(ALLOW_CREATING_WORKSPACES, Boolean.toString(isCreatingWorkspacesAllowed())));
737            String[] workspaceNames = getPredefinedWorkspaceNames();
738            if (workspaceNames != null && workspaceNames.length != 0) {
739                ref.add(new StringRefAddr(PREDEFINED_WORKSPACE_NAMES, StringUtil.combineLines(workspaceNames)));
740            }
741            if (getModel() != null) {
742                ref.add(new StringRefAddr(MODEL_NAME, getModel()));
743            }
744            ref.add(new StringRefAddr(RETRY_LIMIT, Integer.toString(getRetryLimit())));
745            return ref;
746        }
747    
748        /**
749         * {@inheritDoc}
750         */
751        public Object getObjectInstance( Object obj,
752                                         javax.naming.Name name,
753                                         Context nameCtx,
754                                         Hashtable<?, ?> environment ) throws Exception {
755            if (obj instanceof Reference) {
756                Map<String, String> values = new HashMap<String, String>();
757                Reference ref = (Reference)obj;
758                Enumeration<?> en = ref.getAll();
759                while (en.hasMoreElements()) {
760                    RefAddr subref = (RefAddr)en.nextElement();
761                    if (subref instanceof StringRefAddr) {
762                        String key = subref.getType();
763                        Object value = subref.getContent();
764                        if (value != null) values.put(key, value.toString());
765                    }
766                }
767                String sourceName = values.get(SOURCE_NAME);
768                String rootNodeUuid = values.get(ROOT_NODE_UUID);
769                String dataSourceJndiName = values.get(DATA_SOURCE_JNDI_NAME);
770                String dialect = values.get(DIALECT);
771                String username = values.get(USERNAME);
772                String password = values.get(PASSWORD);
773                String url = values.get(URL);
774                String driverClassName = values.get(DRIVER_CLASS_NAME);
775                String driverClassloaderName = values.get(DRIVER_CLASSLOADER_NAME);
776                String maxConnectionsInPool = values.get(MAXIMUM_CONNECTIONS_IN_POOL);
777                String minConnectionsInPool = values.get(MINIMUM_CONNECTIONS_IN_POOL);
778                String maxConnectionIdleTimeInSec = values.get(MAXIMUM_CONNECTION_IDLE_TIME_IN_SECONDS);
779                String maxSizeOfStatementCache = values.get(MAXIMUM_SIZE_OF_STATEMENT_CACHE);
780                String acquisitionIncrement = values.get(NUMBER_OF_CONNECTIONS_TO_BE_ACQUIRED_AS_NEEDED);
781                String idleTimeInSeconds = values.get(IDLE_TIME_IN_SECONDS_BEFORE_TESTING_CONNECTIONS);
782                String cacheTtlInMillis = values.get(CACHE_TIME_TO_LIVE_IN_MILLISECONDS);
783                String modelName = values.get(MODEL_NAME);
784                String retryLimit = values.get(RETRY_LIMIT);
785                String largeModelSize = values.get(LARGE_VALUE_SIZE_IN_BYTES);
786                String compressData = values.get(COMPRESS_DATA);
787                String refIntegrity = values.get(ENFORCE_REFERENTIAL_INTEGRITY);
788                String defaultWorkspace = values.get(DEFAULT_WORKSPACE);
789                String createWorkspaces = values.get(ALLOW_CREATING_WORKSPACES);
790    
791                String combinedWorkspaceNames = values.get(PREDEFINED_WORKSPACE_NAMES);
792                String[] workspaceNames = null;
793                if (combinedWorkspaceNames != null) {
794                    List<String> paths = StringUtil.splitLines(combinedWorkspaceNames);
795                    workspaceNames = paths.toArray(new String[paths.size()]);
796                }
797    
798                // Create the source instance ...
799                JpaSource source = new JpaSource();
800                if (sourceName != null) source.setName(sourceName);
801                if (rootNodeUuid != null) source.setRootNodeUuid(rootNodeUuid);
802                if (dataSourceJndiName != null) source.setDataSourceJndiName(dataSourceJndiName);
803                if (dialect != null) source.setDialect(dialect);
804                if (username != null) source.setUsername(username);
805                if (password != null) source.setPassword(password);
806                if (url != null) source.setUrl(url);
807                if (driverClassName != null) source.setDriverClassName(driverClassName);
808                if (driverClassloaderName != null) source.setDriverClassloaderName(driverClassloaderName);
809                if (maxConnectionsInPool != null) source.setMaximumConnectionsInPool(Integer.parseInt(maxConnectionsInPool));
810                if (minConnectionsInPool != null) source.setMinimumConnectionsInPool(Integer.parseInt(minConnectionsInPool));
811                if (maxConnectionIdleTimeInSec != null) source.setMaximumConnectionIdleTimeInSeconds(Integer.parseInt(maxConnectionIdleTimeInSec));
812                if (maxSizeOfStatementCache != null) source.setMaximumSizeOfStatementCache(Integer.parseInt(maxSizeOfStatementCache));
813                if (acquisitionIncrement != null) source.setNumberOfConnectionsToAcquireAsNeeded(Integer.parseInt(acquisitionIncrement));
814                if (idleTimeInSeconds != null) source.setIdleTimeInSecondsBeforeTestingConnections(Integer.parseInt(idleTimeInSeconds));
815                if (cacheTtlInMillis != null) source.setCacheTimeToLiveInMilliseconds(Integer.parseInt(cacheTtlInMillis));
816                if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit));
817                if (modelName != null) source.setModel(modelName);
818                if (largeModelSize != null) source.setLargeValueSizeInBytes(Long.parseLong(largeModelSize));
819                if (compressData != null) source.setCompressData(Boolean.parseBoolean(compressData));
820                if (refIntegrity != null) source.setReferentialIntegrityEnforced(Boolean.parseBoolean(refIntegrity));
821                if (defaultWorkspace != null) source.setNameOfDefaultWorkspace(defaultWorkspace);
822                if (createWorkspaces != null) source.setCreatingWorkspacesAllowed(Boolean.parseBoolean(createWorkspaces));
823                if (workspaceNames != null && workspaceNames.length != 0) source.setPredefinedWorkspaceNames(workspaceNames);
824                return source;
825            }
826            return null;
827        }
828    
829        /**
830         * {@inheritDoc}
831         * 
832         * @see org.jboss.dna.graph.connector.RepositorySource#getConnection()
833         */
834        public synchronized RepositoryConnection getConnection() throws RepositorySourceException {
835            if (this.name == null || this.name.trim().length() == 0) {
836                throw new RepositorySourceException(JpaConnectorI18n.repositorySourceMustHaveName.text());
837            }
838            assert rootNodeUuid != null;
839            assert rootUuid != null;
840            EntityManager entityManager = null;
841            if (entityManagerFactory == null || !entityManagerFactory.isOpen()) {
842                // Create the JPA EntityManagerFactory by programmatically configuring Hibernate Entity Manager ...
843                Ejb3Configuration configurator = new Ejb3Configuration();
844    
845                // Configure the entity classes ...
846                configurator.addAnnotatedClass(StoreOptionEntity.class);
847                if (model != null) model.configure(configurator);
848    
849                // Configure additional properties, which may be overridden by subclasses ...
850                configure(configurator);
851    
852                // Now set the mandatory information, overwriting anything that the subclasses may have tried ...
853                if (this.dataSource == null && this.dataSourceJndiName != null) {
854                    // Try to load the DataSource from JNDI ...
855                    try {
856                        Context context = new InitialContext();
857                        dataSource = (DataSource)context.lookup(this.dataSourceJndiName);
858                    } catch (Throwable t) {
859                        Logger.getLogger(getClass())
860                              .error(t, JpaConnectorI18n.errorFindingDataSourceInJndi, name, dataSourceJndiName);
861                    }
862                }
863    
864                if (this.dataSource != null) {
865                    // Set the data source ...
866                    configurator.setDataSource(this.dataSource);
867                } else {
868                    // Set the context class loader, so that the driver could be found ...
869                    if (this.repositoryContext != null && this.driverClassloaderName != null) {
870                        try {
871                            ExecutionContext context = this.repositoryContext.getExecutionContext();
872                            ClassLoader loader = context.getClassLoader(this.driverClassloaderName);
873                            if (loader != null) {
874                                Thread.currentThread().setContextClassLoader(loader);
875                            }
876                        } catch (Throwable t) {
877                            I18n msg = JpaConnectorI18n.errorSettingContextClassLoader;
878                            Logger.getLogger(getClass()).error(t, msg, name, driverClassloaderName);
879                        }
880                    }
881                    // Set the connection properties ...
882                    setProperty(configurator, "hibernate.dialect", this.dialect);
883                    setProperty(configurator, "hibernate.connection.driver_class", this.driverClassName);
884                    setProperty(configurator, "hibernate.connection.username", this.username);
885                    setProperty(configurator, "hibernate.connection.password", this.password);
886                    setProperty(configurator, "hibernate.connection.url", this.url);
887                    setProperty(configurator, "hibernate.connection.max_fetch_depth", DEFAULT_MAXIMUM_FETCH_DEPTH);
888                    setProperty(configurator, "hibernate.connection.pool_size", 0); // don't use the built-in pool
889                }
890    
891                entityManagerFactory = configurator.buildEntityManagerFactory();
892    
893                // Establish a connection and obtain the store options...
894                entityManager = entityManagerFactory.createEntityManager();
895    
896                // Find and update/set the root node's UUID ...
897                StoreOptions options = new StoreOptions(entityManager);
898                UUID actualUuid = options.getRootNodeUuid();
899                if (actualUuid != null) {
900                    this.setRootNodeUuid(actualUuid.toString());
901                } else {
902                    options.setRootNodeUuid(this.rootUuid);
903                }
904    
905                // Find or set the type of model that will be used.
906                String actualModelName = options.getModelName();
907                if (actualModelName == null) {
908                    // This is a new store, so set to the specified model ...
909                    if (model == null) setModel(Models.DEFAULT.getName());
910                    assert model != null;
911                    options.setModelName(model);
912                } else {
913                    try {
914                        setModel(actualModelName);
915                    } catch (Throwable e) {
916                        // The actual model name doesn't match what's available in the software ...
917                        entityManagerFactory.close();
918                        String msg = JpaConnectorI18n.existingStoreSpecifiesUnknownModel.text(name, actualModelName);
919                        throw new RepositorySourceException(msg);
920                    }
921                }
922                entityManagerFactory.close();
923    
924                // Now, create another entity manager with the classes from the correct model
925                model.configure(configurator);
926                entityManagerFactory = configurator.buildEntityManagerFactory();
927                entityManager = entityManagerFactory.createEntityManager();
928            }
929            if (entityManager == null) {
930                entityManager = entityManagerFactory.createEntityManager();
931            }
932            return new JpaConnection(getName(), cachePolicy, entityManager, model, rootUuid, defaultWorkspace,
933                                     getPredefinedWorkspaceNames(), largeValueSizeInBytes, isCreatingWorkspacesAllowed(),
934                                     compressData, referentialIntegrityEnforced);
935        }
936    
937        /**
938         * Set up the JPA configuration using Hibernate, except for the entity classes (which will already be configured when this
939         * method is called) and the data source or connection information (which will be set after this method returns). Subclasses
940         * may override this method to customize the configuration.
941         * <p>
942         * This method sets up the C3P0 connection pooling, the cache provider, and some DDL options.
943         * </p>
944         * 
945         * @param configuration the Hibernate configuration; never null
946         */
947        protected void configure( Ejb3Configuration configuration ) {
948            // Set the connection pooling properties (to use C3P0) ...
949            setProperty(configuration, "hibernate.connection.provider_class", "org.hibernate.connection.C3P0ConnectionProvider");
950            setProperty(configuration, "hibernate.c3p0.max_size", this.maximumConnectionsInPool);
951            setProperty(configuration, "hibernate.c3p0.min_size", this.minimumConnectionsInPool);
952            setProperty(configuration, "hibernate.c3p0.timeout", this.idleTimeInSecondsBeforeTestingConnections);
953            setProperty(configuration, "hibernate.c3p0.max_statements", this.maximumSizeOfStatementCache);
954            setProperty(configuration, "hibernate.c3p0.idle_test_period", this.idleTimeInSecondsBeforeTestingConnections);
955            setProperty(configuration, "hibernate.c3p0.acquire_increment", this.numberOfConnectionsToAcquireAsNeeded);
956            setProperty(configuration, "hibernate.c3p0.validate", "false");
957    
958            // Disable the second-level cache ...
959            setProperty(configuration, "hibernate.cache.provider_class", "org.hibernate.cache.NoCacheProvider");
960    
961            // Set up the schema and DDL options ...
962            // setProperty(configuration, "hibernate.show_sql", "true"); // writes all SQL statements to console
963            setProperty(configuration, "hibernate.format_sql", "true");
964            setProperty(configuration, "hibernate.use_sql_comments", "true");
965            setProperty(configuration, "hibernate.hbm2ddl.auto", "create");
966        }
967    
968        /**
969         * Close any resources held by this source. This will ensure that all connections are closed.
970         */
971        public synchronized void close() {
972            if (entityManagerFactory != null) {
973                try {
974                    entityManagerFactory.close();
975                } finally {
976                    entityManagerFactory = null;
977                }
978            }
979        }
980    
981        protected void setProperty( Ejb3Configuration configurator,
982                                    String propertyName,
983                                    String propertyValue ) {
984            assert configurator != null;
985            assert propertyName != null;
986            assert propertyName.trim().length() != 0;
987            if (propertyValue != null) {
988                configurator.setProperty(propertyName, propertyValue.trim());
989            }
990        }
991    
992        protected void setProperty( Ejb3Configuration configurator,
993                                    String propertyName,
994                                    int propertyValue ) {
995            assert configurator != null;
996            assert propertyName != null;
997            assert propertyName.trim().length() != 0;
998            configurator.setProperty(propertyName, Integer.toString(propertyValue));
999        }
1000    
1001        @Immutable
1002        /*package*/class JpaCachePolicy implements CachePolicy {
1003            private static final long serialVersionUID = 1L;
1004            private final int ttl;
1005    
1006            /*package*/JpaCachePolicy( int ttl ) {
1007                this.ttl = ttl;
1008            }
1009    
1010            public long getTimeToLive() {
1011                return ttl;
1012            }
1013    
1014        }
1015    
1016    }