2 * Copyright (C) 2007-2012 Argeo GmbH
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
8 * http://www.apache.org/licenses/LICENSE-2.0
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.
16 package org
.argeo
.jackrabbit
;
18 import java
.io
.ByteArrayInputStream
;
19 import java
.io
.InputStream
;
20 import java
.io
.InputStreamReader
;
21 import java
.io
.Reader
;
23 import java
.util
.ArrayList
;
24 import java
.util
.HashMap
;
25 import java
.util
.List
;
28 import javax
.jcr
.Credentials
;
29 import javax
.jcr
.Node
;
30 import javax
.jcr
.NodeIterator
;
31 import javax
.jcr
.Session
;
32 import javax
.jcr
.nodetype
.NodeType
;
34 import org
.apache
.commons
.io
.FilenameUtils
;
35 import org
.apache
.commons
.io
.IOUtils
;
36 import org
.apache
.commons
.logging
.Log
;
37 import org
.apache
.commons
.logging
.LogFactory
;
38 import org
.apache
.jackrabbit
.commons
.NamespaceHelper
;
39 import org
.apache
.jackrabbit
.commons
.cnd
.CndImporter
;
40 import org
.argeo
.ArgeoException
;
41 import org
.argeo
.jcr
.ArgeoJcrConstants
;
42 import org
.argeo
.jcr
.ArgeoNames
;
43 import org
.argeo
.jcr
.ArgeoTypes
;
44 import org
.argeo
.jcr
.JcrRepositoryWrapper
;
45 import org
.argeo
.jcr
.JcrUtils
;
46 import org
.argeo
.util
.security
.DigestUtils
;
47 import org
.osgi
.framework
.Bundle
;
48 import org
.osgi
.framework
.BundleContext
;
49 import org
.osgi
.framework
.ServiceReference
;
50 import org
.osgi
.service
.packageadmin
.ExportedPackage
;
51 import org
.osgi
.service
.packageadmin
.PackageAdmin
;
52 import org
.springframework
.context
.ResourceLoaderAware
;
53 import org
.springframework
.core
.io
.Resource
;
54 import org
.springframework
.core
.io
.ResourceLoader
;
57 * Wrapper around a Jackrabbit repository which allows to simplify configuration
58 * and intercept some actions. It exposes itself as a {@link Repository}.
60 @SuppressWarnings("deprecation")
61 public class JackrabbitWrapper
extends JcrRepositoryWrapper
implements
63 private final static Log log
= LogFactory
.getLog(JackrabbitWrapper
.class);
64 private final static String DIGEST_ALGORITHM
= "MD5";
67 private ResourceLoader resourceLoader
;
70 /** Node type definitions in CND format */
71 private List
<String
> cndFiles
= new ArrayList
<String
>();
73 * Always import CNDs. Useful during development of new data models. In
74 * production, explicit migration processes should be used.
76 private Boolean forceCndImport
= true;
78 /** Namespaces to register: key is prefix, value namespace */
79 private Map
<String
, String
> namespaces
= new HashMap
<String
, String
>();
81 private BundleContext bundleContext
;
84 * Explicitly set admin credentials used in initialization. Useful for
85 * testing, in real applications authentication is rather dealt with
88 private Credentials adminCredentials
= null;
91 * Empty constructor, {@link #init()} should be called after properties have
94 public JackrabbitWrapper() {
107 * Import declared node type definitions and register namespaces. Tries to
108 * update the node definitions if they have changed. In case of failures an
109 * error will be logged but no exception will be thrown.
111 protected void prepareDataModel() {
112 if ((cndFiles
== null || cndFiles
.size() == 0)
113 && (namespaces
== null || namespaces
.size() == 0))
116 Session session
= null;
118 session
= login(adminCredentials
);
119 // register namespaces
120 if (namespaces
.size() > 0) {
121 NamespaceHelper namespaceHelper
= new NamespaceHelper(session
);
122 namespaceHelper
.registerNamespaces(namespaces
);
125 // load CND files from classpath or as URL
126 for (String resUrl
: cndFiles
) {
127 processCndFile(session
, resUrl
);
129 } catch (Exception e
) {
130 JcrUtils
.discardQuietly(session
);
131 throw new ArgeoException("Cannot import node type definitions "
134 JcrUtils
.logoutQuietly(session
);
139 protected void processCndFile(Session session
, String resUrl
) {
140 Reader reader
= null;
142 // check existing data model nodes
143 new NamespaceHelper(session
).registerNamespace(ArgeoNames
.ARGEO
,
144 ArgeoNames
.ARGEO_NAMESPACE
);
145 if (!session
.itemExists(ArgeoJcrConstants
.DATA_MODELS_BASE_PATH
))
146 JcrUtils
.mkdirs(session
,
147 ArgeoJcrConstants
.DATA_MODELS_BASE_PATH
);
148 Node dataModels
= session
149 .getNode(ArgeoJcrConstants
.DATA_MODELS_BASE_PATH
);
150 NodeIterator it
= dataModels
.getNodes();
151 Node dataModel
= null;
152 while (it
.hasNext()) {
153 Node node
= it
.nextNode();
154 if (node
.getProperty(ArgeoNames
.ARGEO_URI
).getString()
161 byte[] cndContent
= readCndContent(resUrl
);
162 String newDigest
= DigestUtils
.digest(DIGEST_ALGORITHM
, cndContent
);
163 Bundle bundle
= findDataModelBundle(resUrl
);
165 String currentVersion
= null;
166 if (dataModel
!= null) {
167 currentVersion
= dataModel
.getProperty(
168 ArgeoNames
.ARGEO_DATA_MODEL_VERSION
).getString();
169 if (dataModel
.hasNode(Node
.JCR_CONTENT
)) {
170 String oldDigest
= JcrUtils
.checksumFile(dataModel
,
172 if (oldDigest
.equals(newDigest
)) {
173 if (log
.isTraceEnabled())
174 log
.trace("Data model " + resUrl
175 + " hasn't changed, keeping version "
182 if (dataModel
!= null && !forceCndImport
) {
183 log
.info("Data model "
185 + " has changed since version "
187 + (bundle
!= null ?
": version " + bundle
.getVersion()
188 + ", bundle " + bundle
.getSymbolicName() : ""));
192 reader
= new InputStreamReader(new ByteArrayInputStream(cndContent
));
193 // actually imports the CND
195 CndImporter
.registerNodeTypes(reader
, session
, true);
196 } catch (Exception e
) {
197 log
.error("Cannot import data model " + resUrl
, e
);
201 if (dataModel
!= null && !dataModel
.isNodeType(NodeType
.NT_FILE
)) {
206 // FIXME: what if argeo.cnd would not be the first called on
207 // a new repo? argeo:dataModel would not be found
208 String fileName
= FilenameUtils
.getName(resUrl
);
209 if (dataModel
== null) {
210 dataModel
= dataModels
.addNode(fileName
, NodeType
.NT_FILE
);
211 dataModel
.addNode(Node
.JCR_CONTENT
, NodeType
.NT_RESOURCE
);
212 dataModel
.addMixin(ArgeoTypes
.ARGEO_DATA_MODEL
);
213 dataModel
.setProperty(ArgeoNames
.ARGEO_URI
, resUrl
);
215 session
.getWorkspace().getVersionManager()
216 .checkout(dataModel
.getPath());
219 dataModel
.setProperty(ArgeoNames
.ARGEO_DATA_MODEL_VERSION
,
220 bundle
.getVersion().toString());
222 dataModel
.setProperty(ArgeoNames
.ARGEO_DATA_MODEL_VERSION
,
224 JcrUtils
.copyBytesAsFile(dataModel
.getParent(), fileName
,
226 JcrUtils
.updateLastModified(dataModel
);
228 session
.getWorkspace().getVersionManager()
229 .checkin(dataModel
.getPath());
231 if (currentVersion
== null)
232 log
.info("Data model "
234 + (bundle
!= null ?
", version " + bundle
.getVersion()
235 + ", bundle " + bundle
.getSymbolicName() : ""));
237 log
.info("Data model "
239 + " updated from version "
241 + (bundle
!= null ?
", version " + bundle
.getVersion()
242 + ", bundle " + bundle
.getSymbolicName() : ""));
243 } catch (Exception e
) {
244 throw new ArgeoException("Cannot process data model " + resUrl
, e
);
246 IOUtils
.closeQuietly(reader
);
250 protected byte[] readCndContent(String resUrl
) {
251 InputStream in
= null;
255 if (bundleContext
!= null && resUrl
.startsWith("classpath:")) {
256 resUrl
= resUrl
.substring("classpath:".length());
258 } else if (resUrl
.indexOf(':') < 0) {
259 if (!resUrl
.startsWith("/")) {
260 resUrl
= "/" + resUrl
;
261 log
.warn("Classpath should start with '/'");
270 if (bundleContext
!= null) {
271 Bundle currentBundle
= bundleContext
.getBundle();
272 url
= currentBundle
.getResource(resUrl
);
274 resUrl
= "classpath:" + resUrl
;
277 } else if (!resUrl
.startsWith("classpath:")) {
278 url
= new URL(resUrl
);
282 in
= url
.openStream();
283 } else if (resourceLoader
!= null) {
284 Resource res
= resourceLoader
.getResource(resUrl
);
285 in
= res
.getInputStream();
288 throw new ArgeoException("No " + resUrl
+ " in the classpath,"
289 + " make sure the containing" + " package is visible.");
292 return IOUtils
.toByteArray(in
);
293 } catch (Exception e
) {
294 throw new ArgeoException("Cannot read CND from " + resUrl
, e
);
296 IOUtils
.closeQuietly(in
);
301 * REPOSITORY INTERCEPTOR
307 /** Find which OSGi bundle provided the data model resource */
308 protected Bundle
findDataModelBundle(String resUrl
) {
309 if (bundleContext
== null)
312 if (resUrl
.startsWith("/"))
313 resUrl
= resUrl
.substring(1);
314 String pkg
= resUrl
.substring(0, resUrl
.lastIndexOf('/')).replace('/',
316 ServiceReference
<PackageAdmin
> paSr
= bundleContext
317 .getServiceReference(PackageAdmin
.class);
318 PackageAdmin packageAdmin
= (PackageAdmin
) bundleContext
321 // find exported package
322 ExportedPackage exportedPackage
= null;
323 ExportedPackage
[] exportedPackages
= packageAdmin
324 .getExportedPackages(pkg
);
325 if (exportedPackages
== null)
326 throw new ArgeoException("No exported package found for " + pkg
);
327 for (ExportedPackage ep
: exportedPackages
) {
328 for (Bundle b
: ep
.getImportingBundles()) {
329 if (b
.getBundleId() == bundleContext
.getBundle().getBundleId()) {
330 exportedPackage
= ep
;
336 Bundle exportingBundle
= null;
337 if (exportedPackage
!= null) {
338 exportingBundle
= exportedPackage
.getExportingBundle();
340 // assume this is in the same bundle
341 exportingBundle
= bundleContext
.getBundle();
342 // throw new ArgeoException("No OSGi exporting package found for "
345 return exportingBundle
;
351 public void setNamespaces(Map
<String
, String
> namespaces
) {
352 this.namespaces
= namespaces
;
355 public void setCndFiles(List
<String
> cndFiles
) {
356 this.cndFiles
= cndFiles
;
359 public void setBundleContext(BundleContext bundleContext
) {
360 this.bundleContext
= bundleContext
;
363 protected BundleContext
getBundleContext() {
364 return bundleContext
;
367 public void setForceCndImport(Boolean forceCndUpdate
) {
368 this.forceCndImport
= forceCndUpdate
;
371 public void setResourceLoader(ResourceLoader resourceLoader
) {
372 this.resourceLoader
= resourceLoader
;
375 public void setAdminCredentials(Credentials adminCredentials
) {
376 this.adminCredentials
= adminCredentials
;