001    /*
002     * JBoss, Home of Professional Open Source.
003     * Copyright 2008, Red Hat Middleware LLC, and individual contributors
004     * as indicated by the @author tags. See the copyright.txt file in the
005     * distribution for a full listing of individual contributors. 
006     *
007     * This is free software; you can redistribute it and/or modify it
008     * under the terms of the GNU Lesser General Public License as
009     * published by the Free Software Foundation; either version 2.1 of
010     * the License, or (at your option) any later version.
011     *
012     * This software is distributed in the hope that it will be useful,
013     * but WITHOUT ANY WARRANTY; without even the implied warranty of
014     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015     * Lesser General Public License for more details.
016     *
017     * You should have received a copy of the GNU Lesser General Public
018     * License along with this software; if not, write to the Free
019     * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
020     * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
021     */
022    package org.jboss.dna.jcr;
023    
024    import java.lang.reflect.Method;
025    import java.security.AccessControlContext;
026    import java.util.Collections;
027    import java.util.HashMap;
028    import java.util.Map;
029    import java.util.UUID;
030    import javax.jcr.Credentials;
031    import javax.jcr.LoginException;
032    import javax.jcr.Node;
033    import javax.jcr.Repository;
034    import javax.jcr.RepositoryException;
035    import javax.jcr.Session;
036    import javax.jcr.SimpleCredentials;
037    import javax.security.auth.login.LoginContext;
038    import net.jcip.annotations.ThreadSafe;
039    import org.jboss.dna.common.util.CheckArg;
040    import org.jboss.dna.graph.ExecutionContext;
041    import org.jboss.dna.graph.ExecutionContextFactory;
042    import org.jboss.dna.graph.Graph;
043    import org.jboss.dna.graph.connectors.RepositoryConnectionFactory;
044    import com.google.common.base.ReferenceType;
045    import com.google.common.collect.ReferenceMap;
046    
047    /**
048     * Creates JCR {@link Session sessions} to an underlying repository (which may be a federated repository).
049     * <p>
050     * This JCR repository must be configured with the ability to connect to a repository via a supplied
051     * {@link RepositoryConnectionFactory repository connection factory} and repository source name. An
052     * {@link ExecutionContextFactory execution context factory} must also be supplied to enable working with the underlying DNA graph
053     * implementation to which this JCR implementation delegates.
054     * </p>
055     * <p>
056     * If {@link Credentials credentials} are used to login, implementations <em>must</em> also implement one of the following
057     * methods:
058     * 
059     * <pre>
060     * public {@link AccessControlContext} getAccessControlContext();
061     * public {@link LoginContext} getLoginContext();
062     * </pre>
063     * 
064     * Note, {@link Session#getAttributeNames() attributes} on credentials are not supported. JCR {@link SimpleCredentials} are also
065     * not supported.
066     * </p>
067     * 
068     * @author John Verhaeg
069     * @author Randall Hauch
070     */
071    @ThreadSafe
072    public class JcrRepository implements Repository {
073    
074        private final Map<String, String> descriptors;
075        private final ExecutionContextFactory executionContextFactory;
076        private final RepositoryConnectionFactory connectionFactory;
077    
078        /**
079         * Creates a JCR repository that uses the supplied {@link RepositoryConnectionFactory repository connection factory} to
080         * establish {@link Session sessions} to the underlying repository source upon {@link #login() login}.
081         * 
082         * @param executionContextFactory An execution context factory.
083         * @param connectionFactory A repository connection factory.
084         * @throws IllegalArgumentException If <code>executionContextFactory</code> or <code>connectionFactory</code> is
085         *         <code>null</code>.
086         */
087        public JcrRepository( ExecutionContextFactory executionContextFactory,
088                              RepositoryConnectionFactory connectionFactory ) {
089            this(null, executionContextFactory, connectionFactory);
090        }
091    
092        /**
093         * Creates a JCR repository that uses the supplied {@link RepositoryConnectionFactory repository connection factory} to
094         * establish {@link Session sessions} to the underlying repository source upon {@link #login() login}.
095         * 
096         * @param descriptors The {@link #getDescriptorKeys() descriptors} for this repository; may be <code>null</code>.
097         * @param executionContextFactory An execution context factory.
098         * @param connectionFactory A repository connection factory.
099         * @throws IllegalArgumentException If <code>executionContextFactory</code> or <code>connectionFactory</code> is
100         *         <code>null</code>.
101         */
102        public JcrRepository( Map<String, String> descriptors,
103                              ExecutionContextFactory executionContextFactory,
104                              RepositoryConnectionFactory connectionFactory ) {
105            CheckArg.isNotNull(executionContextFactory, "executionContextFactory");
106            CheckArg.isNotNull(connectionFactory, "connectionFactory");
107            this.executionContextFactory = executionContextFactory;
108            this.connectionFactory = connectionFactory;
109            Map<String, String> modifiableDescriptors;
110            if (descriptors == null) {
111                modifiableDescriptors = new HashMap<String, String>();
112            } else {
113                modifiableDescriptors = new HashMap<String, String>(descriptors);
114            }
115            // Initialize required JCR descriptors.
116            modifiableDescriptors.put(Repository.LEVEL_1_SUPPORTED, "true");
117            modifiableDescriptors.put(Repository.LEVEL_2_SUPPORTED, "false");
118            modifiableDescriptors.put(Repository.OPTION_LOCKING_SUPPORTED, "false");
119            modifiableDescriptors.put(Repository.OPTION_OBSERVATION_SUPPORTED, "false");
120            modifiableDescriptors.put(Repository.OPTION_QUERY_SQL_SUPPORTED, "false");
121            modifiableDescriptors.put(Repository.OPTION_TRANSACTIONS_SUPPORTED, "false");
122            modifiableDescriptors.put(Repository.OPTION_VERSIONING_SUPPORTED, "false");
123            modifiableDescriptors.put(Repository.QUERY_XPATH_DOC_ORDER, "true");
124            modifiableDescriptors.put(Repository.QUERY_XPATH_POS_INDEX, "true");
125            // Vendor-specific descriptors (REP_XXX) will only be initialized if not already present, allowing for customer branding.
126            if (!modifiableDescriptors.containsKey(Repository.REP_NAME_DESC)) {
127                modifiableDescriptors.put(Repository.REP_NAME_DESC, JcrI18n.REP_NAME_DESC.text());
128            }
129            if (!modifiableDescriptors.containsKey(Repository.REP_VENDOR_DESC)) {
130                modifiableDescriptors.put(Repository.REP_VENDOR_DESC, JcrI18n.REP_VENDOR_DESC.text());
131            }
132            if (!modifiableDescriptors.containsKey(Repository.REP_VENDOR_URL_DESC)) {
133                modifiableDescriptors.put(Repository.REP_VENDOR_URL_DESC, "http://www.jboss.org/dna");
134            }
135            if (!modifiableDescriptors.containsKey(Repository.REP_VERSION_DESC)) {
136                modifiableDescriptors.put(Repository.REP_VERSION_DESC, "0.2");
137            }
138            modifiableDescriptors.put(Repository.SPEC_NAME_DESC, JcrI18n.SPEC_NAME_DESC.text());
139            modifiableDescriptors.put(Repository.SPEC_VERSION_DESC, "1.0");
140            this.descriptors = Collections.unmodifiableMap(modifiableDescriptors);
141        }
142    
143        /**
144         * {@inheritDoc}
145         * 
146         * @throws IllegalArgumentException if <code>key</code> is <code>null</code>.
147         * @see javax.jcr.Repository#getDescriptor(java.lang.String)
148         */
149        public String getDescriptor( String key ) {
150            CheckArg.isNotEmpty(key, "key");
151            return descriptors.get(key);
152        }
153    
154        /**
155         * {@inheritDoc}
156         * 
157         * @see javax.jcr.Repository#getDescriptorKeys()
158         */
159        public String[] getDescriptorKeys() {
160            return descriptors.keySet().toArray(new String[descriptors.size()]);
161        }
162    
163        /**
164         * {@inheritDoc}
165         * 
166         * @see javax.jcr.Repository#login()
167         */
168        public synchronized Session login() throws RepositoryException {
169            return login(null, null);
170        }
171    
172        /**
173         * {@inheritDoc}
174         * 
175         * @see javax.jcr.Repository#login(javax.jcr.Credentials)
176         */
177        public synchronized Session login( Credentials credentials ) throws RepositoryException {
178            return login(credentials, null);
179        }
180    
181        /**
182         * {@inheritDoc}
183         * 
184         * @see javax.jcr.Repository#login(java.lang.String)
185         */
186        public synchronized Session login( String workspaceName ) throws RepositoryException {
187            return login(null, workspaceName);
188        }
189    
190        /**
191         * {@inheritDoc}
192         * 
193         * @throws IllegalArgumentException if <code>credentials</code> is not <code>null</code> but:
194         *         <ul>
195         *         <li>provides neither a <code>getLoginContext()</code> nor a <code>getAccessControlContext()</code> method.</li>
196         *         <li>provides a <code>getLoginContext()</code> method that doesn't return a {@link LoginContext}.
197         *         <li>provides a <code>getLoginContext()</code> method that returns a <code>null</code> {@link LoginContext}.
198         *         <li>does not provide a <code>getLoginContext()</code> method, but provides a <code>getAccessControlContext()</code>
199         *         method that doesn't return an {@link AccessControlContext}.
200         *         <li>does not provide a <code>getLoginContext()</code> method, but provides a <code>getAccessControlContext()</code>
201         *         method that returns a <code>null</code> {@link AccessControlContext}.
202         *         </ul>
203         * @see javax.jcr.Repository#login(javax.jcr.Credentials, java.lang.String)
204         */
205        public synchronized Session login( Credentials credentials,
206                                           String workspaceName ) throws RepositoryException {
207            // Ensure credentials are either null or provide a JAAS method
208            ExecutionContext execContext;
209            if (credentials == null) {
210                execContext = executionContextFactory.create();
211            } else {
212                try {
213                    // Check if credentials provide a login context
214                    try {
215                        Method method = credentials.getClass().getMethod("getLoginContext");
216                        if (method.getReturnType() != LoginContext.class) {
217                            throw new IllegalArgumentException(JcrI18n.credentialsMustReturnLoginContext.text(credentials.getClass()));
218                        }
219                        LoginContext loginContext = (LoginContext)method.invoke(credentials);
220                        if (loginContext == null) {
221                            throw new IllegalArgumentException(JcrI18n.credentialsMustReturnLoginContext.text(credentials.getClass()));
222                        }
223                        execContext = executionContextFactory.create(loginContext);
224                    } catch (NoSuchMethodException error) {
225                        // Check if credentials provide an access control context
226                        try {
227                            Method method = credentials.getClass().getMethod("getAccessControlContext");
228                            if (method.getReturnType() != AccessControlContext.class) {
229                                throw new IllegalArgumentException(
230                                                                   JcrI18n.credentialsMustReturnAccessControlContext.text(credentials.getClass()));
231                            }
232                            AccessControlContext accessControlContext = (AccessControlContext)method.invoke(credentials);
233                            if (accessControlContext == null) {
234                                throw new IllegalArgumentException(
235                                                                   JcrI18n.credentialsMustReturnAccessControlContext.text(credentials.getClass()));
236                            }
237                            execContext = executionContextFactory.create(accessControlContext);
238                        } catch (NoSuchMethodException error2) {
239                            throw new IllegalArgumentException(JcrI18n.credentialsMustProvideJaasMethod.text(credentials.getClass()),
240                                                               error2);
241                        }
242                    }
243                } catch (RuntimeException error) {
244                    throw error;
245                } catch (Exception error) {
246                    throw new RepositoryException(error);
247                }
248            }
249            // Authenticate if possible
250            assert execContext != null;
251            LoginContext loginContext = execContext.getLoginContext();
252            if (loginContext != null) {
253                try {
254                    loginContext.login();
255                } catch (javax.security.auth.login.LoginException error) {
256                    throw new LoginException(error);
257                }
258            }
259            // Ensure valid workspace name
260            if (workspaceName == null) workspaceName = JcrI18n.defaultWorkspaceName.text();
261            // Create session
262            Graph graph = Graph.create(workspaceName, connectionFactory, execContext);
263            return new JcrSession(this, workspaceName, graph, new ReferenceMap<UUID, Node>(ReferenceType.STRONG, ReferenceType.SOFT));
264        }
265    }