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 }