]> git.argeo.org Git - lgpl/argeo-commons.git/blob - server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitContainer.java
Big cleanup of the security layers
[lgpl/argeo-commons.git] / server / runtime / org.argeo.server.jackrabbit / src / main / java / org / argeo / jackrabbit / 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.File;
20 import java.io.IOException;
21 import java.io.InputStream;
22 import java.io.InputStreamReader;
23 import java.io.Reader;
24 import java.net.URL;
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.SimpleCredentials;
44 import javax.jcr.Value;
45
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;
67
68 /**
69 * Wrapper around a Jackrabbit repository which allows to configure it in Spring
70 * and expose it as a {@link Repository}.
71 */
72 public class JackrabbitContainer implements Repository {
73 private Log log = LogFactory.getLog(JackrabbitContainer.class);
74
75 // remote
76 private String uri = null;
77 private Credentials remoteSystemCredentials = null;
78
79 // local
80 private Resource configuration;
81 private RepositoryConfig repositoryConfig;
82 private File homeDirectory;
83 private Resource variables;
84 private Boolean inMemory = false;
85
86 // wrapped repository
87 private Repository repository;
88
89 // data model
90 /** Node type definitions in CND format */
91 private List<String> cndFiles = new ArrayList<String>();
92
93 /** Migrations to execute (if not already done) */
94 private Set<JackrabbitDataModelMigration> dataModelMigrations = new HashSet<JackrabbitDataModelMigration>();
95
96 /** Namespaces to register: key is prefix, value namespace */
97 private Map<String, String> namespaces = new HashMap<String, String>();
98
99 private Boolean autocreateWorkspaces = false;
100
101 private Executor systemExecutor;
102
103 /**
104 * Empty constructor, {@link #init()} should be called after properties have
105 * been set
106 */
107 public JackrabbitContainer() {
108 }
109
110 /**
111 * Convenience constructor for remote, {@link #init()} is called in the
112 * constructor.
113 */
114 public JackrabbitContainer(String uri, Credentials remoteSystemCredentials) {
115 setUri(uri);
116 setRemoteSystemCredentials(remoteSystemCredentials);
117 init();
118 }
119
120 /** Initializes */
121 public void init() {
122 if (repository != null) {
123 // we are just wrapping another repository
124 prepareDataModel();
125 return;
126 }
127
128 createJackrabbitRepository();
129 // migrate if needed
130 migrate();
131
132 // apply new CND files after migration
133 if (cndFiles != null && cndFiles.size() > 0)
134 prepareDataModel();
135 }
136
137 /** Actually creates the new repository. */
138 protected void createJackrabbitRepository() {
139 long begin = System.currentTimeMillis();
140 InputStream configurationIn = null;
141 try {
142 if (uri != null && !uri.trim().equals("")) {// remote
143 Map<String, String> params = new HashMap<String, String>();
144 params.put(
145 org.apache.jackrabbit.commons.JcrUtils.REPOSITORY_URI,
146 uri);
147 repository = new Jcr2davRepositoryFactory()
148 .getRepository(params);
149 if (repository == null)
150 throw new ArgeoException("Remote Davex repository " + uri
151 + " not found");
152 log.info("Initialized Jackrabbit repository " + repository
153 + " from URI " + uri);
154 // we assume that the remote repository has been properly
155 // configured
156 } else {// local
157 // reset uri to null in order to optimize isRemote()
158 uri = null;
159
160 // temporary
161 if (inMemory && getHomeDirectory().exists()) {
162 FileUtils.deleteDirectory(getHomeDirectory());
163 log.warn("Deleted Jackrabbit home directory "
164 + getHomeDirectory());
165 }
166
167 // process configuration file
168 Properties vars = getConfigurationProperties();
169 configurationIn = configuration.getInputStream();
170 vars.put(
171 RepositoryConfigurationParser.REPOSITORY_HOME_VARIABLE,
172 getHomeDirectory().getCanonicalPath());
173 repositoryConfig = RepositoryConfig.create(new InputSource(
174 configurationIn), vars);
175
176 //
177 // Actual repository creation
178 //
179 repository = RepositoryImpl.create(repositoryConfig);
180
181 double duration = ((double) (System.currentTimeMillis() - begin)) / 1000;
182 log.info("Initialized Jackrabbit repository in " + duration
183 + " s, home: " + getHomeDirectory() + ", config: "
184 + configuration);
185 }
186 } catch (Exception e) {
187 throw new ArgeoException("Cannot create Jackrabbit repository "
188 + getHomeDirectory(), e);
189 } finally {
190 IOUtils.closeQuietly(configurationIn);
191 }
192 }
193
194 /** Executes migrations, if needed. */
195 protected void migrate() {
196 // Remote migration not supported
197 if (isRemote())
198 return;
199
200 // No migration to perform
201 if (dataModelMigrations.size() == 0)
202 return;
203
204 Boolean restartAndClearCaches = false;
205
206 // migrate data
207 Session session = null;
208 try {
209 session = login();
210 for (JackrabbitDataModelMigration dataModelMigration : new TreeSet<JackrabbitDataModelMigration>(
211 dataModelMigrations)) {
212 if (dataModelMigration.migrate(session)) {
213 restartAndClearCaches = true;
214 }
215 }
216 } catch (ArgeoException e) {
217 throw e;
218 } catch (Exception e) {
219 throw new ArgeoException("Cannot migrate", e);
220 } finally {
221 JcrUtils.logoutQuietly(session);
222 }
223
224 // restart repository
225 if (restartAndClearCaches) {
226 JackrabbitDataModelMigration
227 .clearRepositoryCaches(repositoryConfig);
228 ((JackrabbitRepository) repository).shutdown();
229 createJackrabbitRepository();
230 }
231
232 // set data model version
233 try {
234 session = login();
235 } catch (RepositoryException e) {
236 throw new ArgeoException("Cannot login to migrated repository", e);
237 }
238
239 for (JackrabbitDataModelMigration dataModelMigration : new TreeSet<JackrabbitDataModelMigration>(
240 dataModelMigrations)) {
241 try {
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());
249 session.save();
250 }
251 } catch (Exception e) {
252 log.error("Cannot set model version", e);
253 }
254 }
255 JcrUtils.logoutQuietly(session);
256
257 }
258
259 /** Lazy init. */
260 protected File getHomeDirectory() {
261 try {
262 if (homeDirectory == null) {
263 if (inMemory) {
264 homeDirectory = new File(
265 System.getProperty("java.io.tmpdir")
266 + File.separator
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();
273 }
274 }
275
276 return homeDirectory.getCanonicalFile();
277 } catch (IOException e) {
278 throw new ArgeoException("Cannot get canonical file for "
279 + homeDirectory, e);
280 }
281 }
282
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();
288 if (inMemory)
289 if (getHomeDirectory().exists()) {
290 FileUtils.deleteDirectory(getHomeDirectory());
291 if (log.isDebugEnabled())
292 log.debug("Deleted Jackrabbit home directory "
293 + getHomeDirectory());
294 }
295 double duration = ((double) (System.currentTimeMillis() - begin)) / 1000;
296 log.info("Destroyed Jackrabbit repository in " + duration
297 + " s, home: " + getHomeDirectory() + ", config "
298 + configuration);
299 }
300 }
301
302 /**
303 * @deprecated explicitly declare {@link #destroy()} as destroy-method
304 * instead.
305 */
306 public void dispose() throws Exception {
307 log.error("## Declare destroy-method=\"destroy\". in the Jackrabbit container bean");
308 destroy();
309 }
310
311 /*
312 * UTILITIES
313 */
314
315 /** Generates the properties to use in the configuration. */
316 protected Properties getConfigurationProperties() {
317 InputStream propsIn = null;
318 Properties vars;
319 try {
320 vars = new Properties();
321 if (variables != null) {
322 propsIn = variables.getInputStream();
323 vars.load(propsIn);
324 }
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);
331 }
332 // override with system properties
333 vars.putAll(System.getProperties());
334
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()));
339 }
340
341 } catch (IOException e) {
342 throw new ArgeoException("Cannot read configuration properties", e);
343 } finally {
344 IOUtils.closeQuietly(propsIn);
345 }
346 return vars;
347 }
348
349 /**
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.
353 */
354 protected void prepareDataModel() {
355 // importing node def on remote si currently not supported
356 if (isRemote())
357 return;
358
359 Runnable action = new Runnable() {
360 public void run() {
361 Session session = null;
362 try {
363 session = login();
364 // register namespaces
365 if (namespaces.size() > 0) {
366 NamespaceHelper namespaceHelper = new NamespaceHelper(
367 session);
368 namespaceHelper.registerNamespaces(namespaces);
369 }
370 // load CND files from classpath or as URL
371 for (String resUrl : cndFiles) {
372 boolean classpath;
373 if (resUrl.startsWith("classpath:")) {
374 resUrl = resUrl.substring("classpath:".length());
375 classpath = true;
376 } else if (resUrl.indexOf(':') < 0) {
377 classpath = true;
378 } else {
379 classpath = false;
380 }
381
382 URL url = classpath ? getClass().getClassLoader()
383 .getResource(resUrl) : new URL(resUrl);
384
385 Reader reader = null;
386 try {
387 reader = new InputStreamReader(url.openStream());
388 CndImporter
389 .registerNodeTypes(reader, session, true);
390 } finally {
391 IOUtils.closeQuietly(reader);
392 }
393 }
394 } catch (Exception e) {
395 log.error(
396 "Cannot import node type definitions " + cndFiles,
397 e);
398 JcrUtils.discardQuietly(session);
399 } finally {
400 JcrUtils.logoutQuietly(session);
401 }
402 }
403 };
404
405 if (systemExecutor != null)
406 systemExecutor.execute(action);
407 else
408 action.run();
409 }
410
411 /*
412 * DELEGATED JCR REPOSITORY METHODS
413 */
414
415 public String getDescriptor(String key) {
416 return getRepository().getDescriptor(key);
417 }
418
419 public String[] getDescriptorKeys() {
420 return getRepository().getDescriptorKeys();
421 }
422
423 /** Central login method */
424 public Session login(Credentials credentials, String workspaceName)
425 throws LoginException, NoSuchWorkspaceException,
426 RepositoryException {
427
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;
440 }
441 }
442 }
443
444 Session session;
445 try {
446 session = getRepository().login(credentials, workspaceName);
447 } catch (NoSuchWorkspaceException e) {
448 if (autocreateWorkspaces && workspaceName != null)
449 session = createWorkspaceAndLogsIn(credentials, workspaceName);
450 else
451 throw e;
452 }
453 processNewSession(session);
454 return session;
455 }
456
457 public Session login() throws LoginException, RepositoryException {
458 return login(null, null);
459 }
460
461 public Session login(Credentials credentials) throws LoginException,
462 RepositoryException {
463 return login(credentials, null);
464 }
465
466 public Session login(String workspaceName) throws LoginException,
467 NoSuchWorkspaceException, RepositoryException {
468 return login(null, workspaceName);
469 }
470
471 /** Called after a session has been created, does nothing by default. */
472 protected void processNewSession(Session session) {
473 }
474
475 public Boolean isRemote() {
476 return uri != null;
477 }
478
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.");
486 }
487 return repository;
488 }
489
490 /**
491 * Logs in to the default workspace, creates the required workspace, logs
492 * out, logs in to the required workspace.
493 */
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);
500 session.logout();
501 return getRepository().login(credentials, workspaceName);
502 }
503
504 public boolean isStandardDescriptor(String key) {
505 return getRepository().isStandardDescriptor(key);
506 }
507
508 public boolean isSingleValueDescriptor(String key) {
509 return getRepository().isSingleValueDescriptor(key);
510 }
511
512 public Value getDescriptorValue(String key) {
513 return getRepository().getDescriptorValue(key);
514 }
515
516 public Value[] getDescriptorValues(String key) {
517 return getRepository().getDescriptorValues(key);
518 }
519
520 /*
521 * FIELDS ACCESS
522 */
523
524 public void setHomeDirectory(File homeDirectory) {
525 this.homeDirectory = homeDirectory;
526 }
527
528 public void setConfiguration(Resource configuration) {
529 this.configuration = configuration;
530 }
531
532 public void setInMemory(Boolean inMemory) {
533 this.inMemory = inMemory;
534 }
535
536 public void setNamespaces(Map<String, String> namespaces) {
537 this.namespaces = namespaces;
538 }
539
540 public void setCndFiles(List<String> cndFiles) {
541 this.cndFiles = cndFiles;
542 }
543
544 public void setVariables(Resource variables) {
545 this.variables = variables;
546 }
547
548 public void setUri(String uri) {
549 this.uri = uri;
550 }
551
552 public void setRemoteSystemCredentials(Credentials remoteSystemCredentials) {
553 this.remoteSystemCredentials = remoteSystemCredentials;
554 }
555
556 public void setSystemExecutor(Executor systemExecutor) {
557 this.systemExecutor = systemExecutor;
558 }
559
560 public void setRepository(Repository repository) {
561 this.repository = repository;
562 }
563
564 public void setDataModelMigrations(
565 Set<JackrabbitDataModelMigration> dataModelMigrations) {
566 this.dataModelMigrations = dataModelMigrations;
567 }
568 }