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 }