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 }