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