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.jdbc;
025    
026    import java.io.ByteArrayInputStream;
027    import java.io.ByteArrayOutputStream;
028    import java.io.IOException;
029    import java.io.ObjectInputStream;
030    import java.io.ObjectOutputStream;
031    import java.util.Enumeration;
032    import java.util.HashMap;
033    import java.util.Hashtable;
034    import java.util.Map;
035    import java.util.UUID;
036    import java.util.concurrent.atomic.AtomicBoolean;
037    import java.util.concurrent.atomic.AtomicInteger;
038    import javax.naming.BinaryRefAddr;
039    import javax.naming.Context;
040    import javax.naming.Name;
041    import javax.naming.RefAddr;
042    import javax.naming.Reference;
043    import javax.naming.StringRefAddr;
044    import javax.naming.spi.ObjectFactory;
045    
046    import net.jcip.annotations.ThreadSafe;
047    import org.jboss.dna.common.i18n.I18n;
048    import org.jboss.dna.common.jdbc.provider.DataSourceDatabaseMetadataProvider;
049    import org.jboss.dna.common.jdbc.provider.DefaultDataSourceDatabaseMetadataProvider;
050    import org.jboss.dna.common.jdbc.provider.DefaultDriverDatabaseMetadataProvider;
051    import org.jboss.dna.common.jdbc.provider.DriverDatabaseMetadataProvider;
052    import org.jboss.dna.graph.cache.CachePolicy;
053    import org.jboss.dna.graph.connector.RepositoryConnection;
054    import org.jboss.dna.graph.connector.RepositoryContext;
055    import org.jboss.dna.graph.connector.RepositorySource;
056    import org.jboss.dna.graph.connector.RepositorySourceCapabilities;
057    import org.jboss.dna.graph.connector.RepositorySourceException;
058    
059    /**
060     * A description of a JDBC resource that can be used to access database information.
061     * 
062     * @author <a href="mailto:litsenko_sergey@yahoo.com">Sergiy Litsenko</a>
063     */
064    public class JdbcRepositorySource implements RepositorySource, ObjectFactory {
065        private static final long serialVersionUID = 3380130639143030018L;
066    
067        /**
068         * The default limit is {@value} for retrying {@link RepositoryConnection connection} calls to the underlying source.
069         */
070        public static final int DEFAULT_RETRY_LIMIT = 0;
071    
072        /**
073         * This source supports updates by default, but each instance may be configured to {@link #setSupportsUpdates(boolean) be
074         * read-only or updateable}.
075         */
076        public static final boolean DEFAULT_SUPPORTS_UPDATES = true;
077        /**
078         * The default UUID that is used for root nodes in a JDBC connector.
079         */
080        public static final String DEFAULT_ROOT_NODE_UUID = "9f9a52c8-0a4d-40d0-ac58-7c77b24b3155";
081    
082        /**
083         * This source supports events.
084         */
085        protected static final boolean SUPPORTS_EVENTS = true;
086        /**
087         * This source does not support same-name-siblings.
088         */
089        protected static final boolean SUPPORTS_SAME_NAME_SIBLINGS = false;
090    
091        private final AtomicInteger retryLimit = new AtomicInteger(DEFAULT_RETRY_LIMIT);
092        protected String name;
093        protected final Capabilities capabilities = new Capabilities();
094        protected transient RepositoryContext repositoryContext;
095        protected CachePolicy defaultCachePolicy;
096        protected transient DriverDatabaseMetadataProvider driverProvider;
097        protected transient DataSourceDatabaseMetadataProvider dataSourceProvider;
098        protected transient UUID rootUuid = UUID.fromString(DEFAULT_ROOT_NODE_UUID);
099        
100        protected static final String SOURCE_NAME = "sourceName";
101        protected static final String ROOT_NODE_UUID = "rootNodeUuid";
102        protected static final String DEFAULT_CACHE_POLICY = "defaultCachePolicy";
103        protected static final String DATA_SOURCE_JNDI_NAME = "dataSourceJndiName";
104        protected static final String USERNAME = "username";
105        protected static final String PASSWORD = "password";
106        protected static final String URL = "url";
107        protected static final String DRIVER_CLASS_NAME = "driverClassName";
108        protected static final String RETRY_LIMIT = "retryLimit";
109    
110        /**
111         * Get and optionally create driver based provider 
112         * @param create create provider
113         * @return driverProvider
114         */
115        protected DriverDatabaseMetadataProvider getDriverProvider(boolean create) {
116            // lazy creation
117            if (driverProvider == null) {
118                driverProvider = new DefaultDriverDatabaseMetadataProvider();
119            }
120            return driverProvider;
121        }
122    
123        /**
124         * Get and optionally create data source based provider 
125         * @param create create provider
126         * @return dataSourceProvider
127         */
128        protected DataSourceDatabaseMetadataProvider getDataSourceProvider(boolean create) {
129            // lazy creation
130            if (dataSourceProvider == null && create) {
131                dataSourceProvider = new DefaultDataSourceDatabaseMetadataProvider();
132            }
133            return dataSourceProvider;
134        }
135        
136        /**
137         * default constructor
138         */
139        public JdbcRepositorySource() {
140        }
141    
142        /**
143         * {@inheritDoc}
144         * 
145         * @see org.jboss.dna.graph.connector.RepositorySource#getCapabilities()
146         */
147        public RepositorySourceCapabilities getCapabilities() {
148            return capabilities;
149        }
150    
151        /**
152         * {@inheritDoc}
153         * 
154         * @see org.jboss.dna.graph.connector.RepositorySource#getConnection()
155         */
156        public RepositoryConnection getConnection() throws RepositorySourceException {
157            String errMsg = null;
158            // check name
159            if (getName() == null) {
160                errMsg = JdbcMetadataI18n.propertyIsRequired.text("name");
161                throw new RepositorySourceException(errMsg);
162            }
163            
164            // create Jdbc connection using data source first
165            try {
166                if (dataSourceProvider != null) {
167                    // create wrapper for Jdbc connection
168                    return new JdbcConnection(getName(),
169                                              getDefaultCachePolicy(),
170                                              dataSourceProvider.getConnection(),
171                                              rootUuid);
172                }
173            } catch (Exception e) {
174                errMsg = JdbcMetadataI18n.unableToGetConnectionUsingDriver.text(getName(), getDriverClassName(), getDatabaseUrl());
175                throw new RepositorySourceException(errMsg, e);
176            }
177    
178            // create Jdbc connection using driver and database URL
179            try {
180                if (driverProvider != null) {
181                    // create wrapper for Jdbc connection
182                    return new JdbcConnection(getName(),
183                                              getDefaultCachePolicy(),
184                                              driverProvider.getConnection(),
185                                              rootUuid);
186                }
187            } catch (Exception e) {
188                errMsg = JdbcMetadataI18n.unableToGetConnectionUsingDataSource.text(getName(), getDataSourceName());
189                throw new RepositorySourceException(errMsg, e);
190            }
191            
192            // Either data source name or JDBC driver connection properties must be defined
193            errMsg = JdbcMetadataI18n.oneOfPropertiesIsRequired.text(getName());
194            throw new RepositorySourceException(errMsg);
195        }
196    
197        /**
198         * {@inheritDoc}
199         * 
200         * @see org.jboss.dna.graph.connector.RepositorySource#getName()
201         */
202        public String getName() {
203            return name;
204        }
205    
206        /**
207         * Set the name of this source
208         * 
209         * @param name the name for this source
210         */
211        public void setName( String name ) {
212            this.name = name;
213        }
214    
215        /**
216         * {@inheritDoc}
217         * 
218         * @see org.jboss.dna.graph.connector.RepositorySource#getRetryLimit()
219         */
220        public int getRetryLimit() {
221            return retryLimit.get();
222        }
223    
224        /**
225         * {@inheritDoc}
226         * 
227         * @see org.jboss.dna.graph.connector.RepositorySource#initialize(org.jboss.dna.graph.connector.RepositoryContext)
228         */
229        public void initialize( RepositoryContext context ) throws RepositorySourceException {
230            this.repositoryContext = context;
231        }
232    
233        /**
234         * {@inheritDoc}
235         * 
236         * @see org.jboss.dna.graph.connector.RepositorySource#setRetryLimit(int)
237         */
238        public void setRetryLimit( int limit ) {
239            retryLimit.set(limit < 0 ? 0 : limit);
240        }
241    
242        /**
243         * Get whether this source supports updates.
244         * 
245         * @return true if this source supports updates, or false if this source only supports reading content.
246         */
247        public boolean getSupportsUpdates() {
248            return capabilities.supportsUpdates();
249        }
250    
251        /**
252         * Set whether this source supports updates.
253         * 
254         * @param supportsUpdates true if this source supports updating content, or false if this source only supports reading
255         *        content.
256         */
257        public synchronized void setSupportsUpdates( boolean supportsUpdates ) {
258            capabilities.setSupportsUpdates(supportsUpdates);
259        }
260    
261        /**
262         * Get the default cache policy for this source, or null if the global default cache policy should be used
263         * 
264         * @return the default cache policy, or null if this source has no explicit default cache policy
265         */
266        public CachePolicy getDefaultCachePolicy() {
267            return defaultCachePolicy;
268        }
269    
270        /**
271         * @param defaultCachePolicy Sets defaultCachePolicy to the specified value.
272         */
273        public synchronized void setDefaultCachePolicy( CachePolicy defaultCachePolicy ) {
274            this.defaultCachePolicy = defaultCachePolicy;
275        }
276    
277        /**
278         * @return rootNodeUuid
279         */
280        public String getRootNodeUuid() {
281            return rootUuid != null? rootUuid.toString() : null;
282        }
283    
284        /**
285         * @param rootNodeUuid Sets rootNodeUuid to the specified value.
286         * @throws IllegalArgumentException if the string value cannot be converted to UUID
287         */
288        public void setRootNodeUuid( String rootNodeUuid ) {
289            if (rootNodeUuid != null && rootNodeUuid.trim().length() == 0) rootNodeUuid = DEFAULT_ROOT_NODE_UUID;
290            this.rootUuid = UUID.fromString(rootNodeUuid);
291        }
292        
293        /**
294         * {@inheritDoc}
295         */
296        @Override
297        public boolean equals( Object obj ) {
298            if (obj == this) return true;
299            if (obj instanceof JdbcRepositorySource) {
300                JdbcRepositorySource that = (JdbcRepositorySource)obj;
301                if (this.getName() == null) {
302                    if (that.getName() != null) return false;
303                } else {
304                    if (!this.getName().equals(that.getName())) return false;
305                }
306                return true;
307            }
308            return false;
309        }
310    
311        /**
312         * Gets JDBC driver class name
313         * 
314         * @return the JDBC driver class name if any
315         */
316        public String getDriverClassName() {
317            // get provider
318            DriverDatabaseMetadataProvider provider = getDriverProvider(false);
319            // return 
320            return (provider != null)? provider.getDriverClassName() : null;
321        }
322      
323        /**
324         * Sets JDBC driver class name
325         * 
326         * @param driverClassName the JDBC driver class name
327         */
328        public void setDriverClassName( String driverClassName ) {
329            if (driverClassName == null) {
330                driverProvider = null;
331            } else {
332                // get/create provider
333                DriverDatabaseMetadataProvider provider = getDriverProvider(true);
334                // set
335                provider.setDriverClassName(driverClassName);
336            }
337        }
338        
339        /**
340         * Gets database URL as string
341         * 
342         * @return database URL as string
343         */
344        public String getDatabaseUrl() {
345            // get provider
346            DriverDatabaseMetadataProvider provider = getDriverProvider(false);
347            // return 
348            return (provider != null)? provider.getDatabaseUrl() : null;
349        }
350    
351        /**
352         * Sets the database URL as string
353         * 
354         * @param databaseUrl the database URL as string
355         */
356        public void setDatabaseUrl( String databaseUrl ) {
357            if (databaseUrl == null) {
358                driverProvider = null;
359            } else {
360                // get/create provider
361                DriverDatabaseMetadataProvider provider = getDriverProvider(true);
362                // set
363                provider.setDatabaseUrl(databaseUrl);
364            }
365        }
366        
367        /**
368         * Gets the user name
369         * 
370         * @return the user name
371         */
372        public String getUserName() {
373            // get provider
374            DriverDatabaseMetadataProvider provider = getDriverProvider(false);
375            return (provider != null)? provider.getUserName() : null;
376        }
377    
378        /**
379         * Sets the user name
380         * 
381         * @param userName the user name
382         */
383        public void setUserName( String userName ) {
384            if (userName == null) {
385                driverProvider = null;
386            } else {
387                // get provider
388                DriverDatabaseMetadataProvider provider = getDriverProvider(true);
389                provider.setUserName(userName);
390            }
391        }
392        
393        /**
394         * Get user's password
395         * 
396         * @return user's password
397         */
398        public String getPassword() {
399            // get provider
400            DriverDatabaseMetadataProvider provider = getDriverProvider(false);
401            return (provider != null)? provider.getPassword() : null;
402         }
403    
404        /**
405         * Sets the user's password
406         * 
407         * @param password the user's password
408         */
409        public void setPassword( String password ) {
410            if (password == null) {
411                driverProvider = null;
412            } else {
413                // get provider
414                DriverDatabaseMetadataProvider provider = getDriverProvider(true);
415                provider.setPassword(password);
416            }
417        }
418        
419        /**
420         * Sets data source JNDI name
421         * 
422         * @return data source JNDI name
423         */
424        public String getDataSourceName() {
425            // get provider
426            DataSourceDatabaseMetadataProvider provider = getDataSourceProvider(false);
427            return (provider != null)? provider.getDataSourceName() : null;
428        }
429    
430        /**
431         * Sets data source JNDI name
432         * 
433         * @param dataSourceName the data source JNDI name
434         */
435        public void setDataSourceName( String dataSourceName ) {
436            if (dataSourceName == null) {
437                dataSourceProvider = null;
438            } else {
439                // get provider
440                DataSourceDatabaseMetadataProvider provider = getDataSourceProvider(true);
441                provider.setDataSourceName(dataSourceName);
442            }
443        }
444        
445        /**
446         * {@inheritDoc}
447         * 
448         * @see javax.naming.Referenceable#getReference()
449         */
450        public Reference getReference() {
451            String className = getClass().getName();
452            String factoryClassName = this.getClass().getName();
453            Reference ref = new Reference(className, factoryClassName, null);
454    
455            if (getName() != null) {
456                ref.add(new StringRefAddr(SOURCE_NAME, getName()));
457            }
458            
459            if (getRootNodeUuid() != null) {
460                ref.add(new StringRefAddr(ROOT_NODE_UUID, getRootNodeUuid()));
461            }
462            if (getDataSourceName() != null) {
463                ref.add(new StringRefAddr(DATA_SOURCE_JNDI_NAME, getDataSourceName()));
464            }
465            
466            if (getUserName() != null) {
467                ref.add(new StringRefAddr(USERNAME, getUserName()));
468            }
469            
470            if (getPassword() != null) {
471                ref.add(new StringRefAddr(PASSWORD, getPassword()));
472            }
473            
474            if (getDatabaseUrl() != null) {
475                ref.add(new StringRefAddr(URL, getDatabaseUrl()));
476            }
477            if (getDriverClassName() != null) {
478                ref.add(new StringRefAddr(DRIVER_CLASS_NAME, getDriverClassName()));
479            }
480            
481            if (getDefaultCachePolicy() != null) {
482                ByteArrayOutputStream baos = new ByteArrayOutputStream();
483                CachePolicy policy = getDefaultCachePolicy();
484                try {
485                    ObjectOutputStream oos = new ObjectOutputStream(baos);
486                    oos.writeObject(policy);
487                    ref.add(new BinaryRefAddr(DEFAULT_CACHE_POLICY, baos.toByteArray()));
488                } catch (IOException e) {
489                    I18n msg = JdbcMetadataI18n.errorSerializingCachePolicyInSource;
490                    throw new RepositorySourceException(getName(), msg.text(policy.getClass().getName(), getName()), e);
491                }
492            }
493            ref.add(new StringRefAddr(RETRY_LIMIT, Integer.toString(getRetryLimit())));
494            // return it
495            return ref;
496        }
497    
498        /**
499         * {@inheritDoc}
500         * 
501         * @see javax.naming.spi.ObjectFactory#getObjectInstance(java.lang.Object, javax.naming.Name, javax.naming.Context,
502         *      java.util.Hashtable)
503         */
504        public Object getObjectInstance( Object obj,
505                                         Name name,
506                                         Context nameCtx,
507                                         Hashtable<?, ?> environment ) throws Exception {
508            if (obj instanceof Reference) {
509                Map<String, Object> values = new HashMap<String, Object>();
510                Reference ref = (Reference)obj;
511                Enumeration<?> en = ref.getAll();
512                while (en.hasMoreElements()) {
513                    RefAddr subref = (RefAddr)en.nextElement();
514                    if (subref instanceof StringRefAddr) {
515                        String key = subref.getType();
516                        Object value = subref.getContent();
517                        if (value != null) values.put(key, value.toString());
518                    } else if (subref instanceof BinaryRefAddr) {
519                        String key = subref.getType();
520                        Object value = subref.getContent();
521                        if (value instanceof byte[]) {
522                            // Deserialize ...
523                            ByteArrayInputStream bais = new ByteArrayInputStream((byte[])value);
524                            ObjectInputStream ois = new ObjectInputStream(bais);
525                            value = ois.readObject();
526                            values.put(key, value);
527                        }
528                    }
529                }
530                // get individual properties
531                String sourceName = (String)values.get(SOURCE_NAME);
532                String rootNodeUuid = (String)values.get(ROOT_NODE_UUID);
533                String dataSourceJndiName = (String)values.get(DATA_SOURCE_JNDI_NAME);
534                String userName = (String)values.get(USERNAME);
535                String password = (String)values.get(PASSWORD);
536                String url = (String)values.get(URL);
537                String driverClassName = (String)values.get(DRIVER_CLASS_NAME);
538                
539                Object defaultCachePolicy = values.get(DEFAULT_CACHE_POLICY);
540                String retryLimit = (String)values.get(RETRY_LIMIT);
541    
542                // Create the source instance ...
543                JdbcRepositorySource source = new JdbcRepositorySource();
544                if (sourceName != null) source.setName(sourceName);
545                if (rootNodeUuid != null) source.setRootNodeUuid(rootNodeUuid);
546                if (dataSourceJndiName != null) source.setDataSourceName(dataSourceJndiName);
547                if (userName != null) source.setUserName(userName);
548                if (password != null) source.setPassword(password);
549                if (url != null) source.setDatabaseUrl(url);
550                if (driverClassName != null) source.setDriverClassName(driverClassName);
551                if (defaultCachePolicy instanceof CachePolicy) {
552                    source.setDefaultCachePolicy((CachePolicy)defaultCachePolicy);
553                }
554                if (retryLimit != null) source.setRetryLimit(Integer.parseInt(retryLimit));
555                return source;
556            }
557            return null;
558        }
559    
560        @ThreadSafe
561        protected class Capabilities extends RepositorySourceCapabilities {
562            private final AtomicBoolean supportsUpdates = new AtomicBoolean(DEFAULT_SUPPORTS_UPDATES);
563    
564            /*package*/Capabilities() {
565                super(DEFAULT_SUPPORTS_UPDATES, SUPPORTS_EVENTS);
566            }
567    
568            /*package*/void setSupportsUpdates( boolean supportsUpdates ) {
569                this.supportsUpdates.set(supportsUpdates);
570            }
571    
572            @Override
573            public boolean supportsUpdates() {
574                return this.supportsUpdates.get();
575            }
576        }
577    }