]> git.argeo.org Git - lgpl/argeo-commons.git/blob - JackrabbitContainer.java
a708aadd1d67a6eada7401b8bcf94d3cf95ff8ad
[lgpl/argeo-commons.git] / JackrabbitContainer.java
1 /*
2 * Copyright (C) 2010 Mathieu Baudier <mbaudier@argeo.org>
3 *
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
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
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.
15 */
16
17 package org.argeo.jackrabbit;
18
19 import java.io.ByteArrayInputStream;
20 import java.io.File;
21 import java.io.IOException;
22 import java.io.InputStream;
23 import java.io.InputStreamReader;
24 import java.io.Reader;
25 import java.util.ArrayList;
26 import java.util.HashMap;
27 import java.util.HashSet;
28 import java.util.List;
29 import java.util.Map;
30 import java.util.Properties;
31 import java.util.Set;
32 import java.util.TreeSet;
33 import java.util.UUID;
34 import java.util.concurrent.Executor;
35
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.Value;
44
45 import org.apache.commons.io.FileUtils;
46 import org.apache.commons.io.IOUtils;
47 import org.apache.commons.logging.Log;
48 import org.apache.commons.logging.LogFactory;
49 import org.apache.jackrabbit.api.JackrabbitRepository;
50 import org.apache.jackrabbit.commons.NamespaceHelper;
51 import org.apache.jackrabbit.commons.cnd.CndImporter;
52 import org.apache.jackrabbit.core.RepositoryImpl;
53 import org.apache.jackrabbit.core.TransientRepository;
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.springframework.context.ResourceLoaderAware;
61 import org.springframework.core.io.Resource;
62 import org.springframework.core.io.ResourceLoader;
63 import org.springframework.util.SystemPropertyUtils;
64 import org.xml.sax.InputSource;
65
66 /**
67 * Wrapper around a Jackrabbit repository which allows to configure it in Spring
68 * and expose it as a {@link Repository}.
69 */
70 public class JackrabbitContainer implements Repository, ResourceLoaderAware {
71 private Log log = LogFactory.getLog(JackrabbitContainer.class);
72
73 private Resource configuration;
74 private File homeDirectory;
75 private Resource variables;
76
77 private Boolean inMemory = false;
78 private String uri = null;
79
80 // wrapped repository
81 private Repository repository;
82 private RepositoryConfig repositoryConfig;
83
84 // CND
85 private ResourceLoader resourceLoader;
86
87 /** Node type definitions in CND format */
88 private List<String> cndFiles = new ArrayList<String>();
89
90 /** Migrations to execute (if not already done) */
91 private Set<JackrabbitDataModelMigration> dataModelMigrations = new HashSet<JackrabbitDataModelMigration>();
92
93 /** Namespaces to register: key is prefix, value namespace */
94 private Map<String, String> namespaces = new HashMap<String, String>();
95
96 private Boolean autocreateWorkspaces = false;
97
98 private Executor systemExecutor;
99
100 public void init() throws Exception {
101 if (repository != null) {
102 // we are just wrapping another repository
103 importNodeTypeDefinitions(repository);
104 return;
105 }
106
107 createJackrabbitRepository();
108
109 // migrate if needed
110 migrate();
111
112 // apply new CND files after migration
113 if (cndFiles != null && cndFiles.size() > 0)
114 importNodeTypeDefinitions(repository);
115 }
116
117 /** Actually creates a new repository. */
118 protected void createJackrabbitRepository() {
119 long begin = System.currentTimeMillis();
120 try {
121 // remote repository
122 if (uri != null && !uri.trim().equals("")) {
123 Map<String, String> params = new HashMap<String, String>();
124 params.put(
125 org.apache.jackrabbit.commons.JcrUtils.REPOSITORY_URI,
126 uri);
127 repository = new Jcr2davRepositoryFactory()
128 .getRepository(params);
129 if (repository == null)
130 throw new ArgeoException("Remote Davex repository " + uri
131 + " not found");
132 log.info("Initialized Jackrabbit repository " + repository
133 + " from URI " + uri);
134 // do not perform further initialization since we assume that
135 // the
136 // remote repository has been properly configured
137 return;
138 }
139
140 // local repository
141 if (inMemory && getHomeDirectory().exists()) {
142 FileUtils.deleteDirectory(getHomeDirectory());
143 log.warn("Deleted Jackrabbit home directory "
144 + getHomeDirectory());
145 }
146
147 Properties vars = getConfigurationProperties();
148 InputStream in = configuration.getInputStream();
149 try {
150 vars.put(
151 RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE,
152 getHomeDirectory().getCanonicalPath());
153 repositoryConfig = RepositoryConfig.create(new InputSource(in),
154 vars);
155 } catch (Exception e) {
156 throw new RuntimeException("Cannot read configuration", e);
157 } finally {
158 IOUtils.closeQuietly(in);
159 }
160
161 if (inMemory)
162 repository = new TransientRepository(repositoryConfig);
163 else
164 repository = RepositoryImpl.create(repositoryConfig);
165
166 double duration = ((double) (System.currentTimeMillis() - begin)) / 1000;
167 log.info("Initialized Jackrabbit repository in " + duration
168 + " s, home: " + getHomeDirectory() + ", config: "
169 + configuration);
170 } catch (Exception e) {
171 throw new ArgeoException("Cannot create Jackrabbit repository "
172 + getHomeDirectory(), e);
173 }
174 }
175
176 /** Executes migrations, if needed. */
177 protected void migrate() {
178 // No migration to perform
179 if (dataModelMigrations.size() == 0)
180 return;
181
182 Boolean restartAndClearCaches = false;
183
184 // migrate data
185 Session session = null;
186 try {
187 session = login();
188 for (JackrabbitDataModelMigration dataModelMigration : new TreeSet<JackrabbitDataModelMigration>(
189 dataModelMigrations)) {
190 if (dataModelMigration.migrate(session)) {
191 restartAndClearCaches = true;
192 }
193 }
194 } catch (ArgeoException e) {
195 throw e;
196 } catch (Exception e) {
197 throw new ArgeoException("Cannot migrate", e);
198 } finally {
199 JcrUtils.logoutQuietly(session);
200 }
201
202 // restart repository
203 if (restartAndClearCaches) {
204 JackrabbitDataModelMigration
205 .clearRepositoryCaches(repositoryConfig);
206 ((JackrabbitRepository) repository).shutdown();
207 createJackrabbitRepository();
208 }
209
210 // set data model version
211 try {
212 session = login();
213 } catch (RepositoryException e) {
214 throw new ArgeoException("Cannot login to migrated repository", e);
215 }
216
217 for (JackrabbitDataModelMigration dataModelMigration : new TreeSet<JackrabbitDataModelMigration>(
218 dataModelMigrations)) {
219 try {
220 if (session.itemExists(dataModelMigration
221 .getDataModelNodePath())) {
222 Node dataModelNode = session.getNode(dataModelMigration
223 .getDataModelNodePath());
224 dataModelNode.setProperty(
225 ArgeoNames.ARGEO_DATA_MODEL_VERSION,
226 dataModelMigration.getTargetVersion());
227 session.save();
228 }
229 } catch (Exception e) {
230 log.error("Cannot set model version", e);
231 }
232 }
233 JcrUtils.logoutQuietly(session);
234
235 }
236
237 /** Lazy init. */
238 protected File getHomeDirectory() {
239 try {
240 if (homeDirectory == null) {
241 if (inMemory) {
242 homeDirectory = new File(
243 System.getProperty("java.io.tmpdir")
244 + File.separator
245 + System.getProperty("user.name")
246 + File.separator + "jackrabbit-"
247 + UUID.randomUUID());
248 homeDirectory.mkdirs();
249 // will it work if directory is not empty?
250 homeDirectory.deleteOnExit();
251 }
252 }
253
254 return homeDirectory.getCanonicalFile();
255 } catch (IOException e) {
256 throw new ArgeoException("Cannot get canonical file for "
257 + homeDirectory, e);
258 }
259 }
260
261 public void dispose() throws Exception {
262 long begin = System.currentTimeMillis();
263 if (repository != null) {
264 if (repository instanceof JackrabbitRepository)
265 ((JackrabbitRepository) repository).shutdown();
266 else if (repository instanceof RepositoryImpl)
267 ((RepositoryImpl) repository).shutdown();
268 else if (repository instanceof TransientRepository)
269 ((TransientRepository) repository).shutdown();
270 }
271
272 if (inMemory)
273 if (getHomeDirectory().exists()) {
274 FileUtils.deleteDirectory(getHomeDirectory());
275 if (log.isDebugEnabled())
276 log.debug("Deleted Jackrabbit home directory "
277 + getHomeDirectory());
278 }
279
280 double duration = ((double) (System.currentTimeMillis() - begin)) / 1000;
281 if (uri != null && !uri.trim().equals(""))
282 log.info("Destroyed Jackrabbit repository with uri " + uri);
283 else
284 log.info("Destroyed Jackrabbit repository in " + duration
285 + " s, home: " + getHomeDirectory() + ", config "
286 + configuration);
287 }
288
289 /**
290 * @deprecated explicitly declare {@link #dispose()} as destroy-method
291 * instead.
292 */
293 public void destroy() throws Exception {
294 log.error("## Declare destroy-method=\"dispose\". in the Jackrabbit container bean");
295 }
296
297 /** @deprecated explicitly declare {@link #init()} as init-method instead. */
298 public void afterPropertiesSet() throws Exception {
299 log.error("## Declare init-method=\"init\". in the Jackrabbit container bean");
300 }
301
302 protected Properties getConfigurationProperties() {
303 InputStream propsIn = null;
304 Properties vars;
305 try {
306 vars = new Properties();
307 if (variables != null) {
308 propsIn = variables.getInputStream();
309 vars.load(propsIn);
310 }
311 // resolve system properties
312 for (Object key : vars.keySet()) {
313 // TODO: implement a smarter mechanism to resolve nested ${}
314 String newValue = SystemPropertyUtils.resolvePlaceholders(vars
315 .getProperty(key.toString()));
316 vars.put(key, newValue);
317 }
318 // override with system properties
319 vars.putAll(System.getProperties());
320
321 if (log.isTraceEnabled()) {
322 log.trace("Jackrabbit config variables:");
323 for (Object key : new TreeSet<Object>(vars.keySet()))
324 log.trace(key + "=" + vars.getProperty(key.toString()));
325 }
326
327 } catch (IOException e) {
328 throw new ArgeoException("Cannot read configuration properties", e);
329 } finally {
330 IOUtils.closeQuietly(propsIn);
331 }
332 return vars;
333 }
334
335 /**
336 * Import declared node type definitions, trying to update them if they have
337 * changed. In case of failures an error will be logged but no exception
338 * will be thrown.
339 */
340 protected void importNodeTypeDefinitions(final Repository repository) {
341 Runnable action = new Runnable() {
342 public void run() {
343 Reader reader = null;
344 Session session = null;
345 try {
346 session = repository.login();
347 processNewSession(session);
348 // Load cnds as resources
349 for (String resUrl : cndFiles) {
350 Resource res = resourceLoader.getResource(resUrl);
351 byte[] arr = IOUtils.toByteArray(res.getInputStream());
352 reader = new InputStreamReader(
353 new ByteArrayInputStream(arr));
354 CndImporter.registerNodeTypes(reader, session, true);
355 }
356 session.save();
357 } catch (Exception e) {
358 log.error(
359 "Cannot import node type definitions " + cndFiles,
360 e);
361 JcrUtils.discardQuietly(session);
362 } finally {
363 IOUtils.closeQuietly(reader);
364 JcrUtils.logoutQuietly(session);
365 }
366 }
367 };
368
369 if (systemExecutor != null)
370 systemExecutor.execute(action);
371 else
372 action.run();
373 }
374
375 // JCR REPOSITORY (delegated)
376 public String getDescriptor(String key) {
377 return getRepository().getDescriptor(key);
378 }
379
380 public String[] getDescriptorKeys() {
381 return getRepository().getDescriptorKeys();
382 }
383
384 public Session login() throws LoginException, RepositoryException {
385 Session session = getRepository().login();
386 processNewSession(session);
387 return session;
388 }
389
390 public Session login(Credentials credentials, String workspaceName)
391 throws LoginException, NoSuchWorkspaceException,
392 RepositoryException {
393 Session session;
394 try {
395 session = getRepository().login(credentials, workspaceName);
396 } catch (NoSuchWorkspaceException e) {
397 if (autocreateWorkspaces)
398 session = createWorkspaceAndLogsIn(credentials, workspaceName);
399 else
400 throw e;
401 }
402 processNewSession(session);
403 return session;
404 }
405
406 public Session login(Credentials credentials) throws LoginException,
407 RepositoryException {
408 Session session = getRepository().login(credentials);
409 processNewSession(session);
410 return session;
411 }
412
413 public Session login(String workspaceName) throws LoginException,
414 NoSuchWorkspaceException, RepositoryException {
415 Session session;
416 try {
417 session = getRepository().login(workspaceName);
418 } catch (NoSuchWorkspaceException e) {
419 if (autocreateWorkspaces)
420 session = createWorkspaceAndLogsIn(null, workspaceName);
421 else
422 throw e;
423 }
424 processNewSession(session);
425 return session;
426 }
427
428 /** Wraps access to the repository, making sure it is available. */
429 protected Repository getRepository() {
430 if (repository == null) {
431 throw new ArgeoException(
432 "No repository initialized."
433 + " Was the init() method called?"
434 + " The dispose() method should also be called on shutdown.");
435 }
436 return repository;
437 }
438
439 protected synchronized void processNewSession(Session session) {
440 try {
441 NamespaceHelper namespaceHelper = new NamespaceHelper(session);
442 namespaceHelper.registerNamespaces(namespaces);
443 } catch (Exception e) {
444 throw new ArgeoException("Cannot process new session", e);
445 }
446 }
447
448 /**
449 * Logs in to the default workspace, creates the required workspace, logs
450 * out, logs in to the required workspace.
451 */
452 protected Session createWorkspaceAndLogsIn(Credentials credentials,
453 String workspaceName) throws RepositoryException {
454 if (workspaceName == null)
455 throw new ArgeoException("No workspace specified.");
456 Session session = getRepository().login(credentials);
457 session.getWorkspace().createWorkspace(workspaceName);
458 session.logout();
459 return getRepository().login(credentials, workspaceName);
460 }
461
462 public void setResourceLoader(ResourceLoader resourceLoader) {
463 this.resourceLoader = resourceLoader;
464 }
465
466 public boolean isStandardDescriptor(String key) {
467 return getRepository().isStandardDescriptor(key);
468 }
469
470 public boolean isSingleValueDescriptor(String key) {
471 return getRepository().isSingleValueDescriptor(key);
472 }
473
474 public Value getDescriptorValue(String key) {
475 return getRepository().getDescriptorValue(key);
476 }
477
478 public Value[] getDescriptorValues(String key) {
479 return getRepository().getDescriptorValues(key);
480 }
481
482 // BEANS METHODS
483 public void setHomeDirectory(File homeDirectory) {
484 this.homeDirectory = homeDirectory;
485 }
486
487 public void setConfiguration(Resource configuration) {
488 this.configuration = configuration;
489 }
490
491 public void setInMemory(Boolean inMemory) {
492 this.inMemory = inMemory;
493 }
494
495 public void setNamespaces(Map<String, String> namespaces) {
496 this.namespaces = namespaces;
497 }
498
499 public void setCndFiles(List<String> cndFiles) {
500 this.cndFiles = cndFiles;
501 }
502
503 public void setVariables(Resource variables) {
504 this.variables = variables;
505 }
506
507 public void setUri(String uri) {
508 this.uri = uri;
509 }
510
511 public void setSystemExecutor(Executor systemExecutor) {
512 this.systemExecutor = systemExecutor;
513 }
514
515 public void setRepository(Repository repository) {
516 this.repository = repository;
517 }
518
519 public void setDataModelMigrations(
520 Set<JackrabbitDataModelMigration> dataModelMigrations) {
521 this.dataModelMigrations = dataModelMigrations;
522 }
523
524 }