2 * Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org>
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
8 * http://www.apache.org/licenses/LICENSE-2.0
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
17 package org
.argeo
.jackrabbit
;
20 import java
.io
.IOException
;
21 import java
.io
.InputStream
;
22 import java
.io
.InputStreamReader
;
23 import java
.io
.Reader
;
25 import java
.util
.ArrayList
;
26 import java
.util
.HashMap
;
27 import java
.util
.HashSet
;
28 import java
.util
.List
;
30 import java
.util
.Properties
;
32 import java
.util
.TreeSet
;
33 import java
.util
.UUID
;
34 import java
.util
.concurrent
.Executor
;
36 import javax
.jcr
.Credentials
;
37 import javax
.jcr
.LoginException
;
38 import javax
.jcr
.NoSuchWorkspaceException
;
39 import javax
.jcr
.Node
;
40 import javax
.jcr
.Repository
;
41 import javax
.jcr
.RepositoryException
;
42 import javax
.jcr
.Session
;
43 import javax
.jcr
.SimpleCredentials
;
44 import javax
.jcr
.Value
;
46 import org
.apache
.commons
.io
.FileUtils
;
47 import org
.apache
.commons
.io
.IOUtils
;
48 import org
.apache
.commons
.logging
.Log
;
49 import org
.apache
.commons
.logging
.LogFactory
;
50 import org
.apache
.jackrabbit
.api
.JackrabbitRepository
;
51 import org
.apache
.jackrabbit
.commons
.NamespaceHelper
;
52 import org
.apache
.jackrabbit
.commons
.cnd
.CndImporter
;
53 import org
.apache
.jackrabbit
.core
.RepositoryImpl
;
54 import org
.apache
.jackrabbit
.core
.config
.RepositoryConfig
;
55 import org
.apache
.jackrabbit
.core
.config
.RepositoryConfigurationParser
;
56 import org
.apache
.jackrabbit
.jcr2dav
.Jcr2davRepositoryFactory
;
57 import org
.argeo
.ArgeoException
;
58 import org
.argeo
.jcr
.ArgeoNames
;
59 import org
.argeo
.jcr
.JcrUtils
;
60 import org
.argeo
.security
.SystemAuthentication
;
61 import org
.springframework
.core
.io
.Resource
;
62 import org
.springframework
.security
.Authentication
;
63 import org
.springframework
.security
.context
.SecurityContextHolder
;
64 import org
.springframework
.security
.providers
.UsernamePasswordAuthenticationToken
;
65 import org
.springframework
.util
.SystemPropertyUtils
;
66 import org
.xml
.sax
.InputSource
;
69 * Wrapper around a Jackrabbit repository which allows to configure it in Spring
70 * and expose it as a {@link Repository}.
72 public class JackrabbitContainer
implements Repository
{
73 private Log log
= LogFactory
.getLog(JackrabbitContainer
.class);
76 private String uri
= null;
77 private Credentials remoteSystemCredentials
= null;
80 private Resource configuration
;
81 private RepositoryConfig repositoryConfig
;
82 private File homeDirectory
;
83 private Resource variables
;
84 private Boolean inMemory
= false;
87 private Repository repository
;
90 /** Node type definitions in CND format */
91 private List
<String
> cndFiles
= new ArrayList
<String
>();
93 /** Migrations to execute (if not already done) */
94 private Set
<JackrabbitDataModelMigration
> dataModelMigrations
= new HashSet
<JackrabbitDataModelMigration
>();
96 /** Namespaces to register: key is prefix, value namespace */
97 private Map
<String
, String
> namespaces
= new HashMap
<String
, String
>();
99 private Boolean autocreateWorkspaces
= false;
101 private Executor systemExecutor
;
104 * Empty constructor, {@link #init()} should be called after properties have
107 public JackrabbitContainer() {
111 * Convenience constructor for remote, {@link #init()} is called in the
114 public JackrabbitContainer(String uri
, Credentials remoteSystemCredentials
) {
116 setRemoteSystemCredentials(remoteSystemCredentials
);
122 if (repository
!= null) {
123 // we are just wrapping another repository
128 createJackrabbitRepository();
132 // apply new CND files after migration
133 if (cndFiles
!= null && cndFiles
.size() > 0)
137 /** Actually creates the new repository. */
138 protected void createJackrabbitRepository() {
139 long begin
= System
.currentTimeMillis();
140 InputStream configurationIn
= null;
142 if (uri
!= null && !uri
.trim().equals("")) {// remote
143 Map
<String
, String
> params
= new HashMap
<String
, String
>();
145 org
.apache
.jackrabbit
.commons
.JcrUtils
.REPOSITORY_URI
,
147 repository
= new Jcr2davRepositoryFactory()
148 .getRepository(params
);
149 if (repository
== null)
150 throw new ArgeoException("Remote Davex repository " + uri
152 log
.info("Initialized Jackrabbit repository " + repository
153 + " from URI " + uri
);
154 // we assume that the remote repository has been properly
157 // reset uri to null in order to optimize isRemote()
161 if (inMemory
&& getHomeDirectory().exists()) {
162 FileUtils
.deleteDirectory(getHomeDirectory());
163 log
.warn("Deleted Jackrabbit home directory "
164 + getHomeDirectory());
167 // process configuration file
168 Properties vars
= getConfigurationProperties();
169 configurationIn
= configuration
.getInputStream();
171 RepositoryConfigurationParser
.REPOSITORY_HOME_VARIABLE
,
172 getHomeDirectory().getCanonicalPath());
173 repositoryConfig
= RepositoryConfig
.create(new InputSource(
174 configurationIn
), vars
);
177 // Actual repository creation
179 repository
= RepositoryImpl
.create(repositoryConfig
);
181 double duration
= ((double) (System
.currentTimeMillis() - begin
)) / 1000;
182 log
.info("Initialized Jackrabbit repository in " + duration
183 + " s, home: " + getHomeDirectory() + ", config: "
186 } catch (Exception e
) {
187 throw new ArgeoException("Cannot create Jackrabbit repository "
188 + getHomeDirectory(), e
);
190 IOUtils
.closeQuietly(configurationIn
);
194 /** Executes migrations, if needed. */
195 protected void migrate() {
196 // Remote migration not supported
200 // No migration to perform
201 if (dataModelMigrations
.size() == 0)
204 Boolean restartAndClearCaches
= false;
207 Session session
= null;
210 for (JackrabbitDataModelMigration dataModelMigration
: new TreeSet
<JackrabbitDataModelMigration
>(
211 dataModelMigrations
)) {
212 if (dataModelMigration
.migrate(session
)) {
213 restartAndClearCaches
= true;
216 } catch (ArgeoException e
) {
218 } catch (Exception e
) {
219 throw new ArgeoException("Cannot migrate", e
);
221 JcrUtils
.logoutQuietly(session
);
224 // restart repository
225 if (restartAndClearCaches
) {
226 JackrabbitDataModelMigration
227 .clearRepositoryCaches(repositoryConfig
);
228 ((JackrabbitRepository
) repository
).shutdown();
229 createJackrabbitRepository();
232 // set data model version
235 } catch (RepositoryException e
) {
236 throw new ArgeoException("Cannot login to migrated repository", e
);
239 for (JackrabbitDataModelMigration dataModelMigration
: new TreeSet
<JackrabbitDataModelMigration
>(
240 dataModelMigrations
)) {
242 if (session
.itemExists(dataModelMigration
243 .getDataModelNodePath())) {
244 Node dataModelNode
= session
.getNode(dataModelMigration
245 .getDataModelNodePath());
246 dataModelNode
.setProperty(
247 ArgeoNames
.ARGEO_DATA_MODEL_VERSION
,
248 dataModelMigration
.getTargetVersion());
251 } catch (Exception e
) {
252 log
.error("Cannot set model version", e
);
255 JcrUtils
.logoutQuietly(session
);
260 protected File
getHomeDirectory() {
262 if (homeDirectory
== null) {
264 homeDirectory
= new File(
265 System
.getProperty("java.io.tmpdir")
267 + System
.getProperty("user.name")
268 + File
.separator
+ "jackrabbit-"
269 + UUID
.randomUUID());
270 homeDirectory
.mkdirs();
271 // will it work if directory is not empty??
272 homeDirectory
.deleteOnExit();
276 return homeDirectory
.getCanonicalFile();
277 } catch (IOException e
) {
278 throw new ArgeoException("Cannot get canonical file for "
283 /** Shutdown the repository */
284 public void destroy() throws Exception
{
285 if (repository
!= null && repository
instanceof RepositoryImpl
) {
286 long begin
= System
.currentTimeMillis();
287 ((RepositoryImpl
) repository
).shutdown();
289 if (getHomeDirectory().exists()) {
290 FileUtils
.deleteDirectory(getHomeDirectory());
291 if (log
.isDebugEnabled())
292 log
.debug("Deleted Jackrabbit home directory "
293 + getHomeDirectory());
295 double duration
= ((double) (System
.currentTimeMillis() - begin
)) / 1000;
296 log
.info("Destroyed Jackrabbit repository in " + duration
297 + " s, home: " + getHomeDirectory() + ", config "
303 * @deprecated explicitly declare {@link #destroy()} as destroy-method
306 public void dispose() throws Exception
{
307 log
.error("## Declare destroy-method=\"destroy\". in the Jackrabbit container bean");
315 /** Generates the properties to use in the configuration. */
316 protected Properties
getConfigurationProperties() {
317 InputStream propsIn
= null;
320 vars
= new Properties();
321 if (variables
!= null) {
322 propsIn
= variables
.getInputStream();
325 // resolve system properties
326 for (Object key
: vars
.keySet()) {
327 // TODO: implement a smarter mechanism to resolve nested ${}
328 String newValue
= SystemPropertyUtils
.resolvePlaceholders(vars
329 .getProperty(key
.toString()));
330 vars
.put(key
, newValue
);
332 // override with system properties
333 vars
.putAll(System
.getProperties());
335 if (log
.isTraceEnabled()) {
336 log
.trace("Jackrabbit config variables:");
337 for (Object key
: new TreeSet
<Object
>(vars
.keySet()))
338 log
.trace(key
+ "=" + vars
.getProperty(key
.toString()));
341 } catch (IOException e
) {
342 throw new ArgeoException("Cannot read configuration properties", e
);
344 IOUtils
.closeQuietly(propsIn
);
350 * Import declared node type definitions and register namespaces. Tries to
351 * update the node definitions if they have changed. In case of failures an
352 * error will be logged but no exception will be thrown.
354 protected void prepareDataModel() {
355 // importing node def on remote si currently not supported
359 Runnable action
= new Runnable() {
361 Session session
= null;
364 // register namespaces
365 if (namespaces
.size() > 0) {
366 NamespaceHelper namespaceHelper
= new NamespaceHelper(
368 namespaceHelper
.registerNamespaces(namespaces
);
370 // load CND files from classpath or as URL
371 for (String resUrl
: cndFiles
) {
373 if (resUrl
.startsWith("classpath:")) {
374 resUrl
= resUrl
.substring("classpath:".length());
376 } else if (resUrl
.indexOf(':') < 0) {
382 URL url
= classpath ?
getClass().getClassLoader()
383 .getResource(resUrl
) : new URL(resUrl
);
385 Reader reader
= null;
387 reader
= new InputStreamReader(url
.openStream());
389 .registerNodeTypes(reader
, session
, true);
391 IOUtils
.closeQuietly(reader
);
394 } catch (Exception e
) {
396 "Cannot import node type definitions " + cndFiles
,
398 JcrUtils
.discardQuietly(session
);
400 JcrUtils
.logoutQuietly(session
);
405 if (systemExecutor
!= null)
406 systemExecutor
.execute(action
);
412 * DELEGATED JCR REPOSITORY METHODS
415 public String
getDescriptor(String key
) {
416 return getRepository().getDescriptor(key
);
419 public String
[] getDescriptorKeys() {
420 return getRepository().getDescriptorKeys();
423 /** Central login method */
424 public Session
login(Credentials credentials
, String workspaceName
)
425 throws LoginException
, NoSuchWorkspaceException
,
426 RepositoryException
{
428 // retrieve credentials for remote
429 if (credentials
== null && isRemote()) {
430 Authentication authentication
= SecurityContextHolder
.getContext()
431 .getAuthentication();
432 if (authentication
!= null) {
433 if (authentication
instanceof UsernamePasswordAuthenticationToken
) {
434 UsernamePasswordAuthenticationToken upat
= (UsernamePasswordAuthenticationToken
) authentication
;
435 credentials
= new SimpleCredentials(upat
.getName(), upat
436 .getCredentials().toString().toCharArray());
437 } else if ((authentication
instanceof SystemAuthentication
)
438 && remoteSystemCredentials
!= null) {
439 credentials
= remoteSystemCredentials
;
446 session
= getRepository().login(credentials
, workspaceName
);
447 } catch (NoSuchWorkspaceException e
) {
448 if (autocreateWorkspaces
&& workspaceName
!= null)
449 session
= createWorkspaceAndLogsIn(credentials
, workspaceName
);
453 processNewSession(session
);
457 public Session
login() throws LoginException
, RepositoryException
{
458 return login(null, null);
461 public Session
login(Credentials credentials
) throws LoginException
,
462 RepositoryException
{
463 return login(credentials
, null);
466 public Session
login(String workspaceName
) throws LoginException
,
467 NoSuchWorkspaceException
, RepositoryException
{
468 return login(null, workspaceName
);
471 /** Called after a session has been created, does nothing by default. */
472 protected void processNewSession(Session session
) {
475 public Boolean
isRemote() {
479 /** Wraps access to the repository, making sure it is available. */
480 protected Repository
getRepository() {
481 if (repository
== null) {
482 throw new ArgeoException(
483 "No repository initialized."
484 + " Was the init() method called?"
485 + " The dispose() method should also be called on shutdown.");
491 * Logs in to the default workspace, creates the required workspace, logs
492 * out, logs in to the required workspace.
494 protected Session
createWorkspaceAndLogsIn(Credentials credentials
,
495 String workspaceName
) throws RepositoryException
{
496 if (workspaceName
== null)
497 throw new ArgeoException("No workspace specified.");
498 Session session
= getRepository().login(credentials
);
499 session
.getWorkspace().createWorkspace(workspaceName
);
501 return getRepository().login(credentials
, workspaceName
);
504 public boolean isStandardDescriptor(String key
) {
505 return getRepository().isStandardDescriptor(key
);
508 public boolean isSingleValueDescriptor(String key
) {
509 return getRepository().isSingleValueDescriptor(key
);
512 public Value
getDescriptorValue(String key
) {
513 return getRepository().getDescriptorValue(key
);
516 public Value
[] getDescriptorValues(String key
) {
517 return getRepository().getDescriptorValues(key
);
524 public void setHomeDirectory(File homeDirectory
) {
525 this.homeDirectory
= homeDirectory
;
528 public void setConfiguration(Resource configuration
) {
529 this.configuration
= configuration
;
532 public void setInMemory(Boolean inMemory
) {
533 this.inMemory
= inMemory
;
536 public void setNamespaces(Map
<String
, String
> namespaces
) {
537 this.namespaces
= namespaces
;
540 public void setCndFiles(List
<String
> cndFiles
) {
541 this.cndFiles
= cndFiles
;
544 public void setVariables(Resource variables
) {
545 this.variables
= variables
;
548 public void setUri(String uri
) {
552 public void setRemoteSystemCredentials(Credentials remoteSystemCredentials
) {
553 this.remoteSystemCredentials
= remoteSystemCredentials
;
556 public void setSystemExecutor(Executor systemExecutor
) {
557 this.systemExecutor
= systemExecutor
;
560 public void setRepository(Repository repository
) {
561 this.repository
= repository
;
564 public void setDataModelMigrations(
565 Set
<JackrabbitDataModelMigration
> dataModelMigrations
) {
566 this.dataModelMigrations
= dataModelMigrations
;