001/*
002 * ModeShape (http://www.modeshape.org)
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *       http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 */
016package org.modeshape.jdbc;
017
018import java.sql.Connection;
019import java.sql.DriverManager;
020import java.sql.DriverPropertyInfo;
021import java.sql.SQLException;
022import java.sql.SQLFeatureNotSupportedException;
023import java.util.Properties;
024import java.util.Set;
025import javax.jcr.Repository;
026import javax.naming.Context;
027import javax.naming.NamingException;
028import org.modeshape.common.collection.Collections;
029import org.modeshape.common.logging.Logger;
030import org.modeshape.jdbc.delegate.ConnectionInfo;
031import org.modeshape.jdbc.delegate.LocalRepositoryDelegate;
032import org.modeshape.jdbc.delegate.RepositoryDelegate;
033import org.modeshape.jdbc.delegate.RepositoryDelegateFactory;
034
035/**
036 * A JDBC driver implementation that is able to access a JCR repository to query its contents using JCR-SQL2. <h3>Connection URLs</h3>
037 * <p>
038 * The driver accepts the following URL format for accessing a <i>local</i> JCR repository using JNDI:
039 * 
040 * <pre>
041 *     jdbc:jcr:jndi:{jndiName}
042 * </pre>
043 * 
044 * or
045 * 
046 * <pre>
047 *     jdbc:jcr:jndi:{jndiName}?{firstProperty}&amp;{secondProperty}&amp;...
048 * </pre>
049 * 
050 * where
051 * <ul>
052 * <li><strong>{jndiName}</strong> is the JNDI name where the {@link Repository} or {@literal org.modeshape.jcr.api.Repositories}
053 * instance can be found;</li>
054 * <li><strong>{firstProperty}</strong> consists of the first property name followed by '=' followed by the property's value;</li>
055 * <li><strong>{secondProperty}</strong> consists of the second property name followed by '=' followed by the property's value;</li>
056 * </ul>
057 * Note that any use of URL encoding ('%' followed by a two-digit hexadecimal value) will be decoded before being used.
058 * </p>
059 * <p>
060 * Here's an example of a URL that defines a {@link Repository} instance located at "<code>jcr/local</code>" with a repository
061 * name of "repository" and a user, password of "secret", and workspace name of "My Workspace":
062 * 
063 * <pre>
064 *     jdbc:jcr:jndi:jcr/local?repositoryName=repository&user=jsmith&amp;password=secret&amp;workspace=My%20Workspace
065 * </pre>
066 * 
067 * The "repository" property is required only if the object in JNDI is a {@literal org.modeshape.jcr.api.Repositories} object.
068 * </p>
069 * <p>
070 * Note that any use of URL encoding ('%' followed by a two-digit hexadecimal value) will be decoded before being used.
071 * </p>
072 */
073
074public class LocalJcrDriver implements java.sql.Driver {
075    protected static Logger logger = Logger.getLogger("org.modeshape.jdbc"); //$NON-NLS-1$
076
077    public static final String WORKSPACE_PROPERTY_NAME = "workspace";
078    public static final String REPOSITORY_PROPERTY_NAME = "repositoryName";
079    public static final String USERNAME_PROPERTY_NAME = "user";
080    public static final String PASSWORD_PROPERTY_NAME = "password";
081    public static final String TEIID_SUPPORT_PROPERTY_NAME = "teiidsupport";
082
083    protected static final Set<String> ALL_PROPERTY_NAMES = Collections.unmodifiableSet(WORKSPACE_PROPERTY_NAME,
084                                                                                        REPOSITORY_PROPERTY_NAME,
085                                                                                        USERNAME_PROPERTY_NAME,
086                                                                                        PASSWORD_PROPERTY_NAME,
087                                                                                        TEIID_SUPPORT_PROPERTY_NAME);
088
089    /* URL Prefix used for JNDI access */
090    public static final String JNDI_URL_PREFIX = "jdbc:jcr:jndi:";
091
092    private static LocalJcrDriver INSTANCE = new LocalJcrDriver();
093
094    static {
095        try {
096            DriverManager.registerDriver(INSTANCE);
097        } catch (SQLException e) {
098            // Logging
099            logger.error(JdbcLocalI18n.driverErrorRegistering, e.getMessage());
100        }
101    }
102
103    private final JcrContextFactory contextFactory;
104    private final RepositoryDelegateFactory delegateFactory;
105    private final DriverInfo driverInfo;
106
107    /**
108     * No-arg constructor, required by the {@link DriverManager}.
109     */
110    public LocalJcrDriver() {
111        this(null);
112    }
113
114    /**
115     * Create an instance of this driver using the supplied JNDI naming context factory. This is useful for testing, but is
116     * otherwise not generally recommended.
117     * 
118     * @param namingContextFactory the naming context factory; may be null if one should be created automatically
119     */
120    protected LocalJcrDriver( JcrContextFactory namingContextFactory ) {
121        this(LocalRepositoryDelegate.FACTORY, new DriverInfo(JdbcLocalI18n.driverName.text(), JdbcLocalI18n.driverVendor.text(),
122                                                             JdbcLocalI18n.driverVendorUrl.text(),
123                                                             JdbcLocalI18n.driverVersion.text()), namingContextFactory);
124    }
125
126    /**
127     * Constructor for subclasses, that should be called by subclasses no-arg constructor.
128     * 
129     * @param delegateFactory the factory that should be used to create {@link RepositoryDelegate} instances; may not be null
130     * @param driverInfo the information about the driver; may not be null
131     * @param namingContextFactory the naming context factory; may be null if one should be created automatically
132     */
133    protected LocalJcrDriver( RepositoryDelegateFactory delegateFactory,
134                              DriverInfo driverInfo,
135                              JcrContextFactory namingContextFactory ) {
136        assert delegateFactory != null;
137        assert driverInfo != null;
138        this.delegateFactory = delegateFactory;
139        this.driverInfo = driverInfo;
140        this.contextFactory = namingContextFactory;
141    }
142
143    @Override
144    public boolean acceptsURL( String url ) {
145        return delegateFactory.acceptUrl(url);
146    }
147
148    @Override
149    public DriverPropertyInfo[] getPropertyInfo( String url,
150                                                 Properties info ) throws SQLException {
151        // Get the connection information ...
152        RepositoryDelegate repositoryDelegate = delegateFactory.createRepositoryDelegate(url, info, this.contextFactory);
153        ConnectionInfo connectionInfo = repositoryDelegate.getConnectionInfo();
154        return connectionInfo.getPropertyInfos();
155    }
156
157    /**
158     * Get the information describing the connection. This method can be overridden to return a subclass of {@link ConnectionInfo}
159     * that implements {@link ConnectionInfo#getCredentials()} for a specific JCR implementation.
160     * 
161     * @param url the JDBC URL
162     * @param info the JDBC connection properties
163     * @return the connection information, or null if the URL is null or not of the proper format
164     * @throws SQLException
165     */
166    protected ConnectionInfo createConnectionInfo( String url,
167                                                   Properties info ) throws SQLException {
168        RepositoryDelegate repositoryDelegate = delegateFactory.createRepositoryDelegate(url, info, this.contextFactory);
169        return repositoryDelegate.getConnectionInfo();
170    }
171
172    /**
173     * {@inheritDoc}
174     * <p>
175     * Note that if the supplied properties and URL contain properties with the same name, the value from the supplied Properties
176     * object will take precedence.
177     * </p>
178     * 
179     * @see java.sql.Driver#connect(java.lang.String, java.util.Properties)
180     */
181    @Override
182    public Connection connect( String url,
183                               Properties info ) throws SQLException {
184        if (!acceptsURL(url)) {
185            return null;
186        }
187        RepositoryDelegate repositoryDelegate = delegateFactory.createRepositoryDelegate(url, info, this.contextFactory);
188        return repositoryDelegate.createConnection(getDriverInfo());
189    }
190
191    @Override
192    public int getMajorVersion() {
193        return getDriverInfo().getMajorVersion();
194    }
195
196    @Override
197    public int getMinorVersion() {
198        return getDriverInfo().getMinorVersion();
199    }
200
201    public String getVendorName() {
202        return getDriverInfo().getVendorName();
203    }
204
205    public String getVendorUrl() {
206        return getDriverInfo().getVendorUrl();
207    }
208
209    public String getVersion() {
210        return getDriverInfo().getVersion();
211    }
212
213    @Override
214    public boolean jdbcCompliant() {
215        return false;
216    }
217
218    protected final DriverInfo getDriverInfo() {
219        return driverInfo;
220    }
221
222    /**
223     * This method always throws {@link SQLFeatureNotSupportedException}.
224     * <p>
225     * <em>Note:</em> This method is part of the JDBC API in JDK 1.7.
226     * </p>
227     * 
228     * @return the parent logger
229     * @throws SQLFeatureNotSupportedException
230     */
231    @Override
232    public java.util.logging.Logger getParentLogger() throws SQLFeatureNotSupportedException {
233        throw new SQLFeatureNotSupportedException();
234    }
235
236    /**
237     * And interface that can be passed to this driver's constructor to create the JNDI naming context given the set of connection
238     * properties.
239     */
240    public interface JcrContextFactory {
241        /**
242         * Create a JNDI naming context from the supplied connection properties.
243         * 
244         * @param properties the connection properties; may be null or empty
245         * @return the naming context; may not be null
246         * @throws NamingException if there is a problem creating or obtaining the naming context
247         */
248        Context createContext( Properties properties ) throws NamingException;
249    }
250
251}