]> git.argeo.org Git - lgpl/argeo-commons.git/blob - server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitContainer.java
ee8b5e5d8344759dbd23d34adcf1e9fa1d765400
[lgpl/argeo-commons.git] / server / runtime / org.argeo.server.jackrabbit / src / main / java / org / argeo / jackrabbit / JackrabbitContainer.java
1 /*
2 * Copyright (C) 2007-2012 Mathieu Baudier
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 package org.argeo.jackrabbit;
17
18 import java.io.IOException;
19 import java.io.InputStream;
20 import java.io.InputStreamReader;
21 import java.io.Reader;
22 import java.net.URL;
23 import java.util.ArrayList;
24 import java.util.HashMap;
25 import java.util.HashSet;
26 import java.util.List;
27 import java.util.Map;
28 import java.util.Set;
29 import java.util.TreeSet;
30
31 import javax.jcr.Credentials;
32 import javax.jcr.LoginException;
33 import javax.jcr.NoSuchWorkspaceException;
34 import javax.jcr.Node;
35 import javax.jcr.NodeIterator;
36 import javax.jcr.Repository;
37 import javax.jcr.RepositoryException;
38 import javax.jcr.Session;
39 import javax.jcr.SimpleCredentials;
40
41 import org.apache.commons.io.FilenameUtils;
42 import org.apache.commons.io.IOUtils;
43 import org.apache.commons.logging.Log;
44 import org.apache.commons.logging.LogFactory;
45 import org.apache.jackrabbit.api.JackrabbitRepository;
46 import org.apache.jackrabbit.commons.NamespaceHelper;
47 import org.apache.jackrabbit.commons.cnd.CndImporter;
48 import org.apache.jackrabbit.core.RepositoryImpl;
49 import org.argeo.ArgeoException;
50 import org.argeo.jcr.ArgeoJcrConstants;
51 import org.argeo.jcr.ArgeoNames;
52 import org.argeo.jcr.ArgeoTypes;
53 import org.argeo.jcr.JcrUtils;
54 import org.argeo.security.SystemAuthentication;
55 import org.osgi.framework.Bundle;
56 import org.osgi.framework.BundleContext;
57 import org.osgi.framework.ServiceReference;
58 import org.osgi.service.packageadmin.ExportedPackage;
59 import org.osgi.service.packageadmin.PackageAdmin;
60 import org.springframework.context.ResourceLoaderAware;
61 import org.springframework.core.io.Resource;
62 import org.springframework.core.io.ResourceLoader;
63 import org.springframework.security.Authentication;
64 import org.springframework.security.context.SecurityContextHolder;
65 import org.springframework.security.providers.UsernamePasswordAuthenticationToken;
66 import org.springframework.util.SystemPropertyUtils;
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 extends JackrabbitWrapper implements
73 ResourceLoaderAware {
74 private Log log = LogFactory.getLog(JackrabbitContainer.class);
75
76 // remote
77 private Credentials remoteSystemCredentials = null;
78
79 // local
80 private Resource configuration;
81 private Resource variables;
82 private ResourceLoader resourceLoader;
83
84 // data model
85 /** Node type definitions in CND format */
86 private List<String> cndFiles = new ArrayList<String>();
87 /**
88 * Always import CNDs. Useful during development of new data models. In
89 * production, explicit migration processes should be used.
90 */
91 private Boolean forceCndImport = false;
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 BundleContext bundleContext;
100
101 /**
102 * Empty constructor, {@link #init()} should be called after properties have
103 * been set
104 */
105 public JackrabbitContainer() {
106 }
107
108 /**
109 * Convenience constructor for remote, {@link #init()} is called in the
110 * constructor.
111 */
112 public JackrabbitContainer(String uri, Credentials remoteSystemCredentials) {
113 setUri(uri);
114 setRemoteSystemCredentials(remoteSystemCredentials);
115 init();
116 }
117
118 @Override
119 protected void postInitWrapped() {
120 prepareDataModel();
121 }
122
123 @Override
124 protected void postInitNew() {
125 // migrate if needed
126 migrate();
127
128 // apply new CND files after migration
129 if (cndFiles != null && cndFiles.size() > 0)
130 prepareDataModel();
131 }
132
133 /*
134 * DATA MODEL
135 */
136
137 /**
138 * Import declared node type definitions and register namespaces. Tries to
139 * update the node definitions if they have changed. In case of failures an
140 * error will be logged but no exception will be thrown.
141 */
142 protected void prepareDataModel() {
143 // importing node def on remote si currently not supported
144 if (isRemote())
145 return;
146
147 Session session = null;
148 try {
149 if (remoteSystemCredentials == null)
150 session = login();
151 else
152 session = login(remoteSystemCredentials);
153 // register namespaces
154 if (namespaces.size() > 0) {
155 NamespaceHelper namespaceHelper = new NamespaceHelper(session);
156 namespaceHelper.registerNamespaces(namespaces);
157 }
158 // load CND files from classpath or as URL
159 for (String resUrl : cndFiles) {
160 boolean classpath;
161 // normalize URL
162 if (resUrl.startsWith("classpath:")) {
163 resUrl = resUrl.substring("classpath:".length());
164 classpath = true;
165 } else if (resUrl.indexOf(':') < 0) {
166 if (!resUrl.startsWith("/")) {
167 resUrl = "/" + resUrl;
168 log.warn("Classpath should start with '/'");
169 }
170 // resUrl = "classpath:" + resUrl;
171 classpath = true;
172 } else {
173 classpath = false;
174 }
175
176 URL url;
177 Bundle dataModelBundle = null;
178 if (classpath) {
179 if (bundleContext != null) {
180 Bundle currentBundle = bundleContext.getBundle();
181 url = currentBundle.getResource(resUrl);
182 if (url != null) {// found
183 dataModelBundle = findDataModelBundle(resUrl);
184 }
185 } else {
186 url = getClass().getClassLoader().getResource(resUrl);
187 // if (url == null)
188 // url = Thread.currentThread()
189 // .getContextClassLoader()
190 // .getResource(resUrl);
191 }
192 } else {
193 url = new URL(resUrl);
194 }
195
196 // check existing data model nodes
197 new NamespaceHelper(session).registerNamespace(
198 ArgeoNames.ARGEO, ArgeoNames.ARGEO_NAMESPACE);
199 if (!session
200 .itemExists(ArgeoJcrConstants.DATA_MODELS_BASE_PATH))
201 JcrUtils.mkdirs(session,
202 ArgeoJcrConstants.DATA_MODELS_BASE_PATH);
203 Node dataModels = session
204 .getNode(ArgeoJcrConstants.DATA_MODELS_BASE_PATH);
205 NodeIterator it = dataModels.getNodes();
206 Node dataModel = null;
207 while (it.hasNext()) {
208 Node node = it.nextNode();
209 if (node.getProperty(ArgeoNames.ARGEO_URI).getString()
210 .equals(resUrl)) {
211 dataModel = node;
212 break;
213 }
214 }
215
216 // does nothing if data model already registered
217 if (dataModel != null && !forceCndImport) {
218 if (dataModelBundle != null) {
219 String version = dataModel.getProperty(
220 ArgeoNames.ARGEO_DATA_MODEL_VERSION)
221 .getString();
222 String dataModelBundleVersion = dataModelBundle
223 .getVersion().toString();
224 if (!version.equals(dataModelBundleVersion)) {
225 log.warn("Data model with version "
226 + dataModelBundleVersion
227 + " available, current version is "
228 + version);
229 }
230 }
231 // do not implicitly update
232 return;
233 }
234
235 InputStream in = null;
236 Reader reader = null;
237 try {
238 if (url != null) {
239 in = url.openStream();
240 } else if (resourceLoader != null) {
241 Resource res = resourceLoader.getResource(resUrl);
242 in = res.getInputStream();
243 url = res.getURL();
244 } else {
245 throw new ArgeoException("No " + resUrl
246 + " in the classpath,"
247 + " make sure the containing"
248 + " package is visible.");
249 }
250
251 reader = new InputStreamReader(in);
252 // actually imports the CND
253 CndImporter.registerNodeTypes(reader, session, true);
254
255 // FIXME: what if argeo.cnd would not be the first called on
256 // a new repo? argeo:dataModel would not be found
257 String fileName = FilenameUtils.getName(url.getPath());
258 if (dataModel == null) {
259 dataModel = dataModels.addNode(fileName);
260 dataModel.addMixin(ArgeoTypes.ARGEO_DATA_MODEL);
261 dataModel.setProperty(ArgeoNames.ARGEO_URI, resUrl);
262 } else {
263 session.getWorkspace().getVersionManager()
264 .checkout(dataModel.getPath());
265 }
266 if (dataModelBundle != null)
267 dataModel.setProperty(
268 ArgeoNames.ARGEO_DATA_MODEL_VERSION,
269 dataModelBundle.getVersion().toString());
270 else
271 dataModel.setProperty(
272 ArgeoNames.ARGEO_DATA_MODEL_VERSION, "0.0.0");
273 JcrUtils.updateLastModified(dataModel);
274 session.save();
275 session.getWorkspace().getVersionManager()
276 .checkin(dataModel.getPath());
277 } finally {
278 IOUtils.closeQuietly(in);
279 IOUtils.closeQuietly(reader);
280 }
281
282 if (log.isDebugEnabled())
283 log.debug("Data model "
284 + resUrl
285 + (dataModelBundle != null ? ", version "
286 + dataModelBundle.getVersion()
287 + ", bundle "
288 + dataModelBundle.getSymbolicName() : ""));
289 }
290 } catch (Exception e) {
291 JcrUtils.discardQuietly(session);
292 throw new ArgeoException("Cannot import node type definitions "
293 + cndFiles, e);
294 } finally {
295 JcrUtils.logoutQuietly(session);
296 }
297
298 }
299
300 /** Executes migrations, if needed. */
301 protected void migrate() {
302 // Remote migration not supported
303 if (isRemote())
304 return;
305
306 // No migration to perform
307 if (dataModelMigrations.size() == 0)
308 return;
309
310 Boolean restartAndClearCaches = false;
311
312 // migrate data
313 Session session = null;
314 try {
315 session = login();
316 for (JackrabbitDataModelMigration dataModelMigration : new TreeSet<JackrabbitDataModelMigration>(
317 dataModelMigrations)) {
318 if (dataModelMigration.migrate(session)) {
319 restartAndClearCaches = true;
320 }
321 }
322 } catch (ArgeoException e) {
323 throw e;
324 } catch (Exception e) {
325 throw new ArgeoException("Cannot migrate", e);
326 } finally {
327 JcrUtils.logoutQuietly(session);
328 }
329
330 // restart repository
331 if (restartAndClearCaches) {
332 Repository repository = getRepository();
333 if (repository instanceof RepositoryImpl) {
334 JackrabbitDataModelMigration
335 .clearRepositoryCaches(((RepositoryImpl) repository)
336 .getConfig());
337 }
338 ((JackrabbitRepository) repository).shutdown();
339 createJackrabbitRepository();
340 }
341
342 // set data model version
343 try {
344 session = login();
345 } catch (RepositoryException e) {
346 throw new ArgeoException("Cannot login to migrated repository", e);
347 }
348
349 for (JackrabbitDataModelMigration dataModelMigration : new TreeSet<JackrabbitDataModelMigration>(
350 dataModelMigrations)) {
351 try {
352 if (session.itemExists(dataModelMigration
353 .getDataModelNodePath())) {
354 Node dataModelNode = session.getNode(dataModelMigration
355 .getDataModelNodePath());
356 dataModelNode.setProperty(
357 ArgeoNames.ARGEO_DATA_MODEL_VERSION,
358 dataModelMigration.getTargetVersion());
359 session.save();
360 }
361 } catch (Exception e) {
362 log.error("Cannot set model version", e);
363 }
364 }
365 JcrUtils.logoutQuietly(session);
366
367 }
368
369 /*
370 * REPOSITORY INTERCEPTOR
371 */
372 /** Central login method */
373 public Session login(Credentials credentials, String workspaceName)
374 throws LoginException, NoSuchWorkspaceException,
375 RepositoryException {
376
377 // retrieve credentials for remote
378 if (credentials == null && isRemote()) {
379 Authentication authentication = SecurityContextHolder.getContext()
380 .getAuthentication();
381 if (authentication != null) {
382 if (authentication instanceof UsernamePasswordAuthenticationToken) {
383 UsernamePasswordAuthenticationToken upat = (UsernamePasswordAuthenticationToken) authentication;
384 credentials = new SimpleCredentials(upat.getName(), upat
385 .getCredentials().toString().toCharArray());
386 } else if ((authentication instanceof SystemAuthentication)
387 && remoteSystemCredentials != null) {
388 credentials = remoteSystemCredentials;
389 }
390 }
391 }
392
393 return super.login(credentials, workspaceName);
394 }
395
396 /*
397 * UTILITIES
398 */
399
400 @Override
401 protected InputStream readConfiguration() {
402 try {
403 return configuration != null ? configuration.getInputStream()
404 : null;
405 } catch (IOException e) {
406 throw new ArgeoException("Cannot read Jackrabbit configuration "
407 + configuration, e);
408 }
409 }
410
411 @Override
412 protected InputStream readVariables() {
413 try {
414 return variables != null ? variables.getInputStream() : null;
415 } catch (IOException e) {
416 throw new ArgeoException("Cannot read Jackrabbit variables "
417 + variables, e);
418 }
419 }
420
421 @Override
422 protected String resolvePlaceholders(String string,
423 Map<String, String> variables) {
424 return SystemPropertyUtils.resolvePlaceholders(string);
425 }
426
427 /** Find which OSGi bundle provided the data model resource */
428 protected Bundle findDataModelBundle(String resUrl) {
429 if (resUrl.startsWith("/"))
430 resUrl = resUrl.substring(1);
431 String pkg = resUrl.substring(0, resUrl.lastIndexOf('/')).replace('/',
432 '.');
433 ServiceReference paSr = bundleContext
434 .getServiceReference(PackageAdmin.class.getName());
435 PackageAdmin packageAdmin = (PackageAdmin) bundleContext
436 .getService(paSr);
437
438 // find exported package
439 ExportedPackage exportedPackage = null;
440 ExportedPackage[] exportedPackages = packageAdmin
441 .getExportedPackages(pkg);
442 if (exportedPackages == null)
443 throw new ArgeoException("No exported package found for " + pkg);
444 for (ExportedPackage ep : exportedPackages) {
445 for (Bundle b : ep.getImportingBundles()) {
446 if (b.getBundleId() == bundleContext.getBundle().getBundleId()) {
447 exportedPackage = ep;
448 break;
449 }
450 }
451 }
452
453 Bundle exportingBundle = null;
454 if (exportedPackage != null) {
455 exportingBundle = exportedPackage.getExportingBundle();
456 } else {
457 throw new ArgeoException("No OSGi exporting package found for "
458 + resUrl);
459 }
460 return exportingBundle;
461 }
462
463 /*
464 * FIELDS ACCESS
465 */
466 public void setConfiguration(Resource configuration) {
467 this.configuration = configuration;
468 }
469
470 public void setNamespaces(Map<String, String> namespaces) {
471 this.namespaces = namespaces;
472 }
473
474 public void setCndFiles(List<String> cndFiles) {
475 this.cndFiles = cndFiles;
476 }
477
478 public void setVariables(Resource variables) {
479 this.variables = variables;
480 }
481
482 public void setRemoteSystemCredentials(Credentials remoteSystemCredentials) {
483 this.remoteSystemCredentials = remoteSystemCredentials;
484 }
485
486 public void setDataModelMigrations(
487 Set<JackrabbitDataModelMigration> dataModelMigrations) {
488 this.dataModelMigrations = dataModelMigrations;
489 }
490
491 public void setBundleContext(BundleContext bundleContext) {
492 this.bundleContext = bundleContext;
493 }
494
495 public void setForceCndImport(Boolean forceCndUpdate) {
496 this.forceCndImport = forceCndUpdate;
497 }
498
499 public void setResourceLoader(ResourceLoader resourceLoader) {
500 this.resourceLoader = resourceLoader;
501 }
502
503 }