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