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 }