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