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