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.jcr;
025    
026    import java.lang.reflect.Method;
027    import java.security.AccessControlContext;
028    import java.util.Collections;
029    import java.util.EnumMap;
030    import java.util.HashMap;
031    import java.util.Map;
032    import java.util.Set;
033    import javax.jcr.Credentials;
034    import javax.jcr.NoSuchWorkspaceException;
035    import javax.jcr.Repository;
036    import javax.jcr.RepositoryException;
037    import javax.jcr.Session;
038    import javax.jcr.SimpleCredentials;
039    import javax.security.auth.login.LoginContext;
040    import net.jcip.annotations.ThreadSafe;
041    import org.jboss.dna.common.util.CheckArg;
042    import org.jboss.dna.graph.ExecutionContext;
043    import org.jboss.dna.graph.Graph;
044    import org.jboss.dna.graph.connector.RepositoryConnectionFactory;
045    import org.jboss.dna.graph.connector.RepositorySourceException;
046    import org.jboss.dna.graph.request.InvalidWorkspaceException;
047    
048    /**
049     * Creates JCR {@link Session sessions} to an underlying repository (which may be a federated repository).
050     * <p>
051     * This JCR repository must be configured with the ability to connect to a repository via a supplied
052     * {@link RepositoryConnectionFactory repository connection factory} and repository source name. An {@link ExecutionContext
053     * execution context} must also be supplied to enable working with the underlying DNA graph implementation to which this JCR
054     * implementation delegates.
055     * </p>
056     * <p>
057     * If {@link Credentials credentials} are used to login, implementations <em>must</em> also implement one of the following
058     * methods:
059     * 
060     * <pre>
061     * public {@link AccessControlContext} getAccessControlContext();
062     * public {@link LoginContext} getLoginContext();
063     * </pre>
064     * 
065     * Note, {@link Session#getAttributeNames() attributes} on credentials are not supported. JCR {@link SimpleCredentials} are also
066     * not supported.
067     * </p>
068     * 
069     * @author John Verhaeg
070     * @author Randall Hauch
071     */
072    @ThreadSafe
073    public class JcrRepository implements Repository {
074    
075        /**
076         * The available options for the {@code JcrRepository}.
077         */
078        public enum Options {
079    
080            /**
081             * Flag that defines whether or not the node types should be exposed as content under the "{@code
082             * /jcr:system/jcr:nodeTypes}" node. Value is either "<code>true</code>" or "<code>false</code>" (default).
083             * 
084             * @see DefaultOptions#PROJECT_NODE_TYPES
085             */
086            PROJECT_NODE_TYPES,
087        }
088    
089        /**
090         * The default values for each of the {@link Options}.
091         */
092        public static class DefaultOptions {
093            /**
094             * The default value for the {@link Options#PROJECT_NODE_TYPES} option is {@value} .
095             */
096            public static final String PROJECT_NODE_TYPES = Boolean.FALSE.toString();
097        }
098    
099        /**
100         * The static unmodifiable map of default options, which are initialized in the static initializer.
101         */
102        protected static final Map<Options, String> DEFAULT_OPTIONS;
103    
104        static {
105            // Initialize the unmodifiable map of default options ...
106            EnumMap<Options, String> defaults = new EnumMap<Options, String>(Options.class);
107            defaults.put(Options.PROJECT_NODE_TYPES, DefaultOptions.PROJECT_NODE_TYPES);
108            DEFAULT_OPTIONS = Collections.<Options, String>unmodifiableMap(defaults);
109        }
110    
111        private final String sourceName;
112        private final Map<String, String> descriptors;
113        private final ExecutionContext executionContext;
114        private final RepositoryConnectionFactory connectionFactory;
115        private final RepositoryNodeTypeManager repositoryTypeManager;
116        private final Map<Options, String> options;
117    
118        /**
119         * Creates a JCR repository that uses the supplied {@link RepositoryConnectionFactory repository connection factory} to
120         * establish {@link Session sessions} to the underlying repository source upon {@link #login() login}.
121         * 
122         * @param executionContext An execution context.
123         * @param connectionFactory A repository connection factory.
124         * @param repositorySourceName the name of the repository source (in the connection factory) that should be used
125         * @throws IllegalArgumentException If <code>executionContextFactory</code> or <code>connectionFactory</code> is
126         *         <code>null</code>.
127         */
128        public JcrRepository( ExecutionContext executionContext,
129                              RepositoryConnectionFactory connectionFactory,
130                              String repositorySourceName ) {
131            this(executionContext, connectionFactory, repositorySourceName, null, null);
132        }
133    
134        /**
135         * Creates a JCR repository that uses the supplied {@link RepositoryConnectionFactory repository connection factory} to
136         * establish {@link Session sessions} to the underlying repository source upon {@link #login() login}.
137         * 
138         * @param executionContext the execution context in which this repository is to operate
139         * @param connectionFactory the factory for repository connections
140         * @param repositorySourceName the name of the repository source (in the connection factory) that should be used
141         * @param descriptors the {@link #getDescriptorKeys() descriptors} for this repository; may be <code>null</code>.
142         * @param options the optional {@link Options settings} for this repository; may be null
143         * @throws IllegalArgumentException If <code>executionContextFactory</code> or <code>connectionFactory</code> is
144         *         <code>null</code>.
145         */
146        public JcrRepository( ExecutionContext executionContext,
147                              RepositoryConnectionFactory connectionFactory,
148                              String repositorySourceName,
149                              Map<String, String> descriptors,
150                              Map<Options, String> options ) {
151            CheckArg.isNotNull(executionContext, "executionContext");
152            CheckArg.isNotNull(connectionFactory, "connectionFactory");
153            CheckArg.isNotNull(repositorySourceName, "repositorySourceName");
154            this.executionContext = executionContext;
155            this.connectionFactory = connectionFactory;
156            this.sourceName = repositorySourceName;
157            Map<String, String> modifiableDescriptors;
158            if (descriptors == null) {
159                modifiableDescriptors = new HashMap<String, String>();
160            } else {
161                modifiableDescriptors = new HashMap<String, String>(descriptors);
162            }
163            // Initialize required JCR descriptors.
164            modifiableDescriptors.put(Repository.LEVEL_1_SUPPORTED, "true");
165            modifiableDescriptors.put(Repository.LEVEL_2_SUPPORTED, "true");
166            modifiableDescriptors.put(Repository.OPTION_LOCKING_SUPPORTED, "false");
167            modifiableDescriptors.put(Repository.OPTION_OBSERVATION_SUPPORTED, "false");
168            modifiableDescriptors.put(Repository.OPTION_QUERY_SQL_SUPPORTED, "false");
169            modifiableDescriptors.put(Repository.OPTION_TRANSACTIONS_SUPPORTED, "false");
170            modifiableDescriptors.put(Repository.OPTION_VERSIONING_SUPPORTED, "false");
171            modifiableDescriptors.put(Repository.QUERY_XPATH_DOC_ORDER, "true");
172            modifiableDescriptors.put(Repository.QUERY_XPATH_POS_INDEX, "true");
173            // Vendor-specific descriptors (REP_XXX) will only be initialized if not already present, allowing for customer branding.
174            if (!modifiableDescriptors.containsKey(Repository.REP_NAME_DESC)) {
175                modifiableDescriptors.put(Repository.REP_NAME_DESC, JcrI18n.REP_NAME_DESC.text());
176            }
177            if (!modifiableDescriptors.containsKey(Repository.REP_VENDOR_DESC)) {
178                modifiableDescriptors.put(Repository.REP_VENDOR_DESC, JcrI18n.REP_VENDOR_DESC.text());
179            }
180            if (!modifiableDescriptors.containsKey(Repository.REP_VENDOR_URL_DESC)) {
181                modifiableDescriptors.put(Repository.REP_VENDOR_URL_DESC, "http://www.jboss.org/dna");
182            }
183            if (!modifiableDescriptors.containsKey(Repository.REP_VERSION_DESC)) {
184                modifiableDescriptors.put(Repository.REP_VERSION_DESC, "0.4");
185            }
186            modifiableDescriptors.put(Repository.SPEC_NAME_DESC, JcrI18n.SPEC_NAME_DESC.text());
187            modifiableDescriptors.put(Repository.SPEC_VERSION_DESC, "1.0");
188            this.descriptors = Collections.unmodifiableMap(modifiableDescriptors);
189    
190            JcrNodeTypeSource source = null;
191            source = new JcrBuiltinNodeTypeSource(this.executionContext);
192            source = new DnaBuiltinNodeTypeSource(this.executionContext, source);
193            this.repositoryTypeManager = new RepositoryNodeTypeManager(this.executionContext, source);
194    
195            if (options == null) {
196                this.options = DEFAULT_OPTIONS;
197            } else {
198                // Initialize with defaults, then add supplied options ...
199                EnumMap<Options, String> localOptions = new EnumMap<Options, String>(DEFAULT_OPTIONS);
200                localOptions.putAll(options);
201                this.options = Collections.unmodifiableMap(localOptions);
202            }
203        }
204    
205        /**
206         * Returns the repository-level node type manager
207         * 
208         * @return the repository-level node type manager
209         */
210        RepositoryNodeTypeManager getRepositoryTypeManager() {
211            return repositoryTypeManager;
212        }
213    
214        /**
215         * Get the options as configured for this repository.
216         * 
217         * @return the unmodifiable options; never null
218         */
219        public Map<Options, String> getOptions() {
220            return options;
221        }
222    
223        /**
224         * Get the name of the repository source that this repository is using.
225         * 
226         * @return the name of the RepositorySource
227         * @see #getConnectionFactory()
228         */
229        String getRepositorySourceName() {
230            return sourceName;
231        }
232    
233        /**
234         * Get the connection factory that this repository is using.
235         * 
236         * @return the connection factory; never null
237         */
238        RepositoryConnectionFactory getConnectionFactory() {
239            return this.connectionFactory;
240        }
241    
242        /**
243         * {@inheritDoc}
244         * 
245         * @throws IllegalArgumentException if <code>key</code> is <code>null</code>.
246         * @see javax.jcr.Repository#getDescriptor(java.lang.String)
247         */
248        public String getDescriptor( String key ) {
249            CheckArg.isNotEmpty(key, "key");
250            return descriptors.get(key);
251        }
252    
253        /**
254         * {@inheritDoc}
255         * 
256         * @see javax.jcr.Repository#getDescriptorKeys()
257         */
258        public String[] getDescriptorKeys() {
259            return descriptors.keySet().toArray(new String[descriptors.size()]);
260        }
261    
262        /**
263         * {@inheritDoc}
264         * 
265         * @see javax.jcr.Repository#login()
266         */
267        public synchronized Session login() throws RepositoryException {
268            return login(null, null);
269        }
270    
271        /**
272         * {@inheritDoc}
273         * 
274         * @see javax.jcr.Repository#login(javax.jcr.Credentials)
275         */
276        public synchronized Session login( Credentials credentials ) throws RepositoryException {
277            return login(credentials, null);
278        }
279    
280        /**
281         * {@inheritDoc}
282         * 
283         * @see javax.jcr.Repository#login(java.lang.String)
284         */
285        public synchronized Session login( String workspaceName ) throws RepositoryException {
286            return login(null, workspaceName);
287        }
288    
289        /**
290         * {@inheritDoc}
291         * 
292         * @throws IllegalArgumentException if <code>credentials</code> is not <code>null</code> but:
293         *         <ul>
294         *         <li>provides neither a <code>getLoginContext()</code> nor a <code>getAccessControlContext()</code> method.</li>
295         *         <li>provides a <code>getLoginContext()</code> method that doesn't return a {@link LoginContext}.
296         *         <li>provides a <code>getLoginContext()</code> method that returns a <code>null</code> {@link LoginContext}.
297         *         <li>does not provide a <code>getLoginContext()</code> method, but provides a <code>getAccessControlContext()</code>
298         *         method that doesn't return an {@link AccessControlContext}.
299         *         <li>does not provide a <code>getLoginContext()</code> method, but provides a <code>getAccessControlContext()</code>
300         *         method that returns a <code>null</code> {@link AccessControlContext}.
301         *         </ul>
302         * @see javax.jcr.Repository#login(javax.jcr.Credentials, java.lang.String)
303         */
304        public synchronized Session login( Credentials credentials,
305                                           String workspaceName ) throws RepositoryException {
306            // Ensure credentials are either null or provide a JAAS method
307            Map<String, Object> sessionAttributes = new HashMap<String, Object>();
308            ExecutionContext execContext = null;
309            if (credentials == null) {
310                execContext = executionContext;
311            } else {
312                try {
313                    // Check if credentials provide a login context
314                    try {
315                        Method method = credentials.getClass().getMethod("getLoginContext");
316                        if (method.getReturnType() != LoginContext.class) {
317                            throw new IllegalArgumentException(JcrI18n.credentialsMustReturnLoginContext.text(credentials.getClass()));
318                        }
319                        LoginContext loginContext = (LoginContext)method.invoke(credentials);
320                        if (loginContext == null) {
321                            throw new IllegalArgumentException(JcrI18n.credentialsMustReturnLoginContext.text(credentials.getClass()));
322                        }
323                        execContext = executionContext.create(loginContext);
324                    } catch (NoSuchMethodException error) {
325                        // Check if credentials provide an access control context
326                        try {
327                            Method method = credentials.getClass().getMethod("getAccessControlContext");
328                            if (method.getReturnType() != AccessControlContext.class) {
329                                throw new IllegalArgumentException(
330                                                                   JcrI18n.credentialsMustReturnAccessControlContext.text(credentials.getClass()));
331                            }
332                            AccessControlContext accessControlContext = (AccessControlContext)method.invoke(credentials);
333                            if (accessControlContext == null) {
334                                throw new IllegalArgumentException(
335                                                                   JcrI18n.credentialsMustReturnAccessControlContext.text(credentials.getClass()));
336                            }
337                            execContext = executionContext.create(accessControlContext);
338                        } catch (NoSuchMethodException error2) {
339                            throw new IllegalArgumentException(JcrI18n.credentialsMustProvideJaasMethod.text(credentials.getClass()),
340                                                               error2);
341                        }
342                    }
343                } catch (RuntimeException error) {
344                    throw error;
345                } catch (Exception error) {
346                    throw new RepositoryException(error);
347                }
348                if (credentials instanceof SimpleCredentials) {
349                    SimpleCredentials simple = (SimpleCredentials)credentials;
350                    for (String attributeName : simple.getAttributeNames()) {
351                        Object attributeValue = simple.getAttribute(attributeName);
352                        sessionAttributes.put(attributeName, attributeValue);
353                    }
354                }
355            }
356    
357            // Ensure valid workspace name
358            Graph graph = Graph.create(sourceName, connectionFactory, executionContext);
359            if (workspaceName == null) {
360                try {
361                    // Get the correct workspace name given the desired workspace name (which may be null) ...
362                    workspaceName = graph.getCurrentWorkspace().getName();
363                } catch (RepositorySourceException e) {
364                    throw new RepositoryException(JcrI18n.errorObtainingDefaultWorkspaceName.text(sourceName, e.getMessage()), e);
365                }
366            } else {
367                try {
368                    // Verify that the workspace exists (or can be created) ...
369                    Set<String> workspaces = graph.getWorkspaces();
370                    if (!workspaces.contains(workspaceName)) {
371                        // Per JCR 1.0 6.1.1, if the workspaceName is not recognized, a NoSuchWorkspaceException is thrown
372                        throw new NoSuchWorkspaceException(JcrI18n.workspaceNameIsInvalid.text(sourceName, workspaceName));
373                    }
374                    graph.useWorkspace(workspaceName);
375                } catch (InvalidWorkspaceException e) {
376                    throw new NoSuchWorkspaceException(JcrI18n.workspaceNameIsInvalid.text(sourceName, workspaceName), e);
377                } catch (RepositorySourceException e) {
378                    String msg = JcrI18n.errorVerifyingWorkspaceName.text(sourceName, workspaceName, e.getMessage());
379                    throw new NoSuchWorkspaceException(msg, e);
380                }
381            }
382    
383            // Create the workspace, which will create its own session ...
384            sessionAttributes = Collections.unmodifiableMap(sessionAttributes);
385            JcrWorkspace workspace = new JcrWorkspace(this, workspaceName, execContext, sessionAttributes);
386            return workspace.getSession();
387        }
388    }