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.connectors.RepositoryConnectionFactory;
043    import com.google.common.base.ReferenceType;
044    import com.google.common.collect.ReferenceMap;
045    
046    /**
047     * Creates JCR {@link Session sessions} to an underlying repository (which may be a federated repository).
048     * <p>
049     * This JCR repository must be configured with the ability to connect to a repository via a supplied
050     * {@link RepositoryConnectionFactory repository connection factory} and repository source name. An
051     * {@link ExecutionContextFactory execution context factory} must also be supplied to enable working with the underlying DNA graph
052     * implementation to which this JCR implementation delegates.
053     * </p>
054     * <p>
055     * If {@link Credentials credentials} are used to login, implementations <em>must</em> also implement one of the following
056     * methods:
057     * 
058     * <pre>
059     * public {@link AccessControlContext} getAccessControlContext();
060     * public {@link LoginContext} getLoginContext();
061     * </pre>
062     * 
063     * Note, {@link Session#getAttributeNames() attributes} on credentials are not supported. JCR {@link SimpleCredentials} are also
064     * not supported.
065     * </p>
066     * 
067     * @author John Verhaeg
068     * @author Randall Hauch
069     */
070    @ThreadSafe
071    public class JcrRepository implements Repository {
072    
073        private final Map<String, String> descriptors;
074        private final ExecutionContextFactory executionContextFactory;
075        private final RepositoryConnectionFactory connectionFactory;
076    
077        /**
078         * Creates a JCR repository that uses the supplied {@link RepositoryConnectionFactory repository connection factory} to
079         * establish {@link Session sessions} to the underlying repository source upon {@link #login() login}.
080         * 
081         * @param executionContextFactory An execution context factory.
082         * @param connectionFactory A repository connection factory.
083         * @throws IllegalArgumentException If <code>executionContextFactory</code> or <code>connectionFactory</code> is
084         *         <code>null</code>.
085         */
086        public JcrRepository( ExecutionContextFactory executionContextFactory,
087                              RepositoryConnectionFactory connectionFactory ) {
088            this(null, executionContextFactory, connectionFactory);
089        }
090    
091        /**
092         * Creates a JCR repository that uses the supplied {@link RepositoryConnectionFactory repository connection factory} to
093         * establish {@link Session sessions} to the underlying repository source upon {@link #login() login}.
094         * 
095         * @param descriptors The {@link #getDescriptorKeys() descriptors} for this repository; may be <code>null</code>.
096         * @param executionContextFactory An execution context factory.
097         * @param connectionFactory A repository connection factory.
098         * @throws IllegalArgumentException If <code>executionContextFactory</code> or <code>connectionFactory</code> is
099         *         <code>null</code>.
100         */
101        public JcrRepository( Map<String, String> descriptors,
102                              ExecutionContextFactory executionContextFactory,
103                              RepositoryConnectionFactory connectionFactory ) {
104            CheckArg.isNotNull(executionContextFactory, "executionContextFactory");
105            CheckArg.isNotNull(connectionFactory, "connectionFactory");
106            this.executionContextFactory = executionContextFactory;
107            this.connectionFactory = connectionFactory;
108            Map<String, String> modifiableDescriptors;
109            if (descriptors == null) {
110                modifiableDescriptors = new HashMap<String, String>();
111            } else {
112                modifiableDescriptors = new HashMap<String, String>(descriptors);
113            }
114            // Initialize required JCR descriptors.
115            modifiableDescriptors.put(Repository.LEVEL_1_SUPPORTED, "true");
116            modifiableDescriptors.put(Repository.LEVEL_2_SUPPORTED, "false");
117            modifiableDescriptors.put(Repository.OPTION_LOCKING_SUPPORTED, "false");
118            modifiableDescriptors.put(Repository.OPTION_OBSERVATION_SUPPORTED, "false");
119            modifiableDescriptors.put(Repository.OPTION_QUERY_SQL_SUPPORTED, "false");
120            modifiableDescriptors.put(Repository.OPTION_TRANSACTIONS_SUPPORTED, "false");
121            modifiableDescriptors.put(Repository.OPTION_VERSIONING_SUPPORTED, "false");
122            modifiableDescriptors.put(Repository.QUERY_XPATH_DOC_ORDER, "true");
123            modifiableDescriptors.put(Repository.QUERY_XPATH_POS_INDEX, "true");
124            // Vendor-specific descriptors (REP_XXX) will only be initialized if not already present, allowing for customer branding.
125            if (!modifiableDescriptors.containsKey(Repository.REP_NAME_DESC)) {
126                modifiableDescriptors.put(Repository.REP_NAME_DESC, JcrI18n.REP_NAME_DESC.text());
127            }
128            if (!modifiableDescriptors.containsKey(Repository.REP_VENDOR_DESC)) {
129                modifiableDescriptors.put(Repository.REP_VENDOR_DESC, JcrI18n.REP_VENDOR_DESC.text());
130            }
131            if (!modifiableDescriptors.containsKey(Repository.REP_VENDOR_URL_DESC)) {
132                modifiableDescriptors.put(Repository.REP_VENDOR_URL_DESC, "http://www.jboss.org/dna");
133            }
134            if (!modifiableDescriptors.containsKey(Repository.REP_VERSION_DESC)) {
135                modifiableDescriptors.put(Repository.REP_VERSION_DESC, "0.2");
136            }
137            modifiableDescriptors.put(Repository.SPEC_NAME_DESC, JcrI18n.SPEC_NAME_DESC.text());
138            modifiableDescriptors.put(Repository.SPEC_VERSION_DESC, "1.0");
139            this.descriptors = Collections.unmodifiableMap(modifiableDescriptors);
140        }
141    
142        /**
143         * {@inheritDoc}
144         * 
145         * @throws IllegalArgumentException if <code>key</code> is <code>null</code>.
146         * @see javax.jcr.Repository#getDescriptor(java.lang.String)
147         */
148        public String getDescriptor( String key ) {
149            CheckArg.isNotEmpty(key, "key");
150            return descriptors.get(key);
151        }
152    
153        /**
154         * {@inheritDoc}
155         * 
156         * @see javax.jcr.Repository#getDescriptorKeys()
157         */
158        public String[] getDescriptorKeys() {
159            return descriptors.keySet().toArray(new String[descriptors.size()]);
160        }
161    
162        /**
163         * {@inheritDoc}
164         * 
165         * @see javax.jcr.Repository#login()
166         */
167        public synchronized Session login() throws RepositoryException {
168            return login(null, null);
169        }
170    
171        /**
172         * {@inheritDoc}
173         * 
174         * @see javax.jcr.Repository#login(javax.jcr.Credentials)
175         */
176        public synchronized Session login( Credentials credentials ) throws RepositoryException {
177            return login(credentials, null);
178        }
179    
180        /**
181         * {@inheritDoc}
182         * 
183         * @see javax.jcr.Repository#login(java.lang.String)
184         */
185        public synchronized Session login( String workspaceName ) throws RepositoryException {
186            return login(null, workspaceName);
187        }
188    
189        /**
190         * {@inheritDoc}
191         * 
192         * @throws IllegalArgumentException if <code>credentials</code> is not <code>null</code> but:
193         *         <ul>
194         *         <li>provides neither a <code>getLoginContext()</code> nor a <code>getAccessControlContext()</code> method.</li>
195         *         <li>provides a <code>getLoginContext()</code> method that doesn't return a {@link LoginContext}.
196         *         <li>provides a <code>getLoginContext()</code> method that returns a <code>null</code> {@link LoginContext}.
197         *         <li>does not provide a <code>getLoginContext()</code> method, but provides a <code>getAccessControlContext()</code>
198         *         method that doesn't return an {@link AccessControlContext}.
199         *         <li>does not provide a <code>getLoginContext()</code> method, but provides a <code>getAccessControlContext()</code>
200         *         method that returns a <code>null</code> {@link AccessControlContext}.
201         *         </ul>
202         * @see javax.jcr.Repository#login(javax.jcr.Credentials, java.lang.String)
203         */
204        public synchronized Session login( Credentials credentials,
205                                           String workspaceName ) throws RepositoryException {
206            // Ensure credentials are either null or provide a JAAS method
207            ExecutionContext execContext;
208            if (credentials == null) {
209                execContext = executionContextFactory.create();
210            } else {
211                try {
212                    // Check if credentials provide a login context
213                    try {
214                        Method method = credentials.getClass().getMethod("getLoginContext");
215                        if (method.getReturnType() != LoginContext.class) {
216                            throw new IllegalArgumentException(JcrI18n.credentialsMustReturnLoginContext.text(credentials.getClass()));
217                        }
218                        LoginContext loginContext = (LoginContext)method.invoke(credentials);
219                        if (loginContext == null) {
220                            throw new IllegalArgumentException(JcrI18n.credentialsMustReturnLoginContext.text(credentials.getClass()));
221                        }
222                        execContext = executionContextFactory.create(loginContext);
223                    } catch (NoSuchMethodException error) {
224                        // Check if credentials provide an access control context
225                        try {
226                            Method method = credentials.getClass().getMethod("getAccessControlContext");
227                            if (method.getReturnType() != AccessControlContext.class) {
228                                throw new IllegalArgumentException(
229                                                                   JcrI18n.credentialsMustReturnAccessControlContext.text(credentials.getClass()));
230                            }
231                            AccessControlContext accessControlContext = (AccessControlContext)method.invoke(credentials);
232                            if (accessControlContext == null) {
233                                throw new IllegalArgumentException(
234                                                                   JcrI18n.credentialsMustReturnAccessControlContext.text(credentials.getClass()));
235                            }
236                            execContext = executionContextFactory.create(accessControlContext);
237                        } catch (NoSuchMethodException error2) {
238                            throw new IllegalArgumentException(JcrI18n.credentialsMustProvideJaasMethod.text(credentials.getClass()),
239                                                               error2);
240                        }
241                    }
242                } catch (RuntimeException error) {
243                    throw error;
244                } catch (Exception error) {
245                    throw new RepositoryException(error);
246                }
247            }
248            // Authenticate if possible
249            assert execContext != null;
250            LoginContext loginContext = execContext.getLoginContext();
251            if (loginContext != null) {
252                try {
253                    loginContext.login();
254                } catch (javax.security.auth.login.LoginException error) {
255                    throw new LoginException(error);
256                }
257            }
258            // Ensure valid workspace name
259            if (workspaceName == null) workspaceName = JcrI18n.defaultWorkspaceName.text();
260            // Create session
261            return new JcrSession(this, execContext, workspaceName, connectionFactory.createConnection(workspaceName),
262                                  new ReferenceMap<UUID, Node>(ReferenceType.STRONG, ReferenceType.SOFT));
263        }
264    }