]> git.argeo.org Git - lgpl/argeo-commons.git/blob - JackrabbitWrapper.java
a9f97d44eb0bacd9bcfa651812eda5265b60591e
[lgpl/argeo-commons.git] / JackrabbitWrapper.java
1 /*
2 * Copyright (C) 2007-2012 Argeo GmbH
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.ByteArrayInputStream;
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.List;
26 import java.util.Map;
27
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;
33
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;
55
56 /**
57 * Wrapper around a Jackrabbit repository which allows to simplify configuration
58 * and intercept some actions. It exposes itself as a {@link Repository}.
59 */
60 @SuppressWarnings("deprecation")
61 public class JackrabbitWrapper extends JcrRepositoryWrapper implements
62 ResourceLoaderAware {
63 private final static Log log = LogFactory.getLog(JackrabbitWrapper.class);
64 private final static String DIGEST_ALGORITHM = "MD5";
65
66 // local
67 private ResourceLoader resourceLoader;
68
69 // data model
70 /** Node type definitions in CND format */
71 private List<String> cndFiles = new ArrayList<String>();
72 /**
73 * Always import CNDs. Useful during development of new data models. In
74 * production, explicit migration processes should be used.
75 */
76 private Boolean forceCndImport = true;
77
78 /** Namespaces to register: key is prefix, value namespace */
79 private Map<String, String> namespaces = new HashMap<String, String>();
80
81 private BundleContext bundleContext;
82
83 /**
84 * Explicitly set admin credentials used in initialization. Useful for
85 * testing, in real applications authentication is rather dealt with
86 * externally
87 */
88 private Credentials adminCredentials = null;
89
90 /**
91 * Empty constructor, {@link #init()} should be called after properties have
92 * been set
93 */
94 public JackrabbitWrapper() {
95 }
96
97 @Override
98 public void init() {
99 prepareDataModel();
100 }
101
102 /*
103 * DATA MODEL
104 */
105
106 /**
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.
110 */
111 protected void prepareDataModel() {
112 if ((cndFiles == null || cndFiles.size() == 0)
113 && (namespaces == null || namespaces.size() == 0))
114 return;
115
116 Session session = null;
117 try {
118 session = login(adminCredentials);
119 // register namespaces
120 if (namespaces.size() > 0) {
121 NamespaceHelper namespaceHelper = new NamespaceHelper(session);
122 namespaceHelper.registerNamespaces(namespaces);
123 }
124
125 // load CND files from classpath or as URL
126 for (String resUrl : cndFiles) {
127 processCndFile(session, resUrl);
128 }
129 } catch (Exception e) {
130 JcrUtils.discardQuietly(session);
131 throw new ArgeoException("Cannot import node type definitions "
132 + cndFiles, e);
133 } finally {
134 JcrUtils.logoutQuietly(session);
135 }
136
137 }
138
139 protected void processCndFile(Session session, String resUrl) {
140 Reader reader = null;
141 try {
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()
155 .equals(resUrl)) {
156 dataModel = node;
157 break;
158 }
159 }
160
161 byte[] cndContent = readCndContent(resUrl);
162 String newDigest = DigestUtils.digest(DIGEST_ALGORITHM, cndContent);
163 Bundle bundle = findDataModelBundle(resUrl);
164
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,
171 DIGEST_ALGORITHM);
172 if (oldDigest.equals(newDigest)) {
173 if (log.isTraceEnabled())
174 log.trace("Data model " + resUrl
175 + " hasn't changed, keeping version "
176 + currentVersion);
177 return;
178 }
179 }
180 }
181
182 if (dataModel != null && !forceCndImport) {
183 log.info("Data model "
184 + resUrl
185 + " has changed since version "
186 + currentVersion
187 + (bundle != null ? ": version " + bundle.getVersion()
188 + ", bundle " + bundle.getSymbolicName() : ""));
189 return;
190 }
191
192 reader = new InputStreamReader(new ByteArrayInputStream(cndContent));
193 // actually imports the CND
194 try {
195 CndImporter.registerNodeTypes(reader, session, true);
196 } catch (Exception e) {
197 log.error("Cannot import data model " + resUrl, e);
198 return;
199 }
200
201 if (dataModel != null && !dataModel.isNodeType(NodeType.NT_FILE)) {
202 dataModel.remove();
203 dataModel = null;
204 }
205
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);
214 } else {
215 session.getWorkspace().getVersionManager()
216 .checkout(dataModel.getPath());
217 }
218 if (bundle != null)
219 dataModel.setProperty(ArgeoNames.ARGEO_DATA_MODEL_VERSION,
220 bundle.getVersion().toString());
221 else
222 dataModel.setProperty(ArgeoNames.ARGEO_DATA_MODEL_VERSION,
223 "0.0.0");
224 JcrUtils.copyBytesAsFile(dataModel.getParent(), fileName,
225 cndContent);
226 JcrUtils.updateLastModified(dataModel);
227 session.save();
228 session.getWorkspace().getVersionManager()
229 .checkin(dataModel.getPath());
230
231 if (currentVersion == null)
232 log.info("Data model "
233 + resUrl
234 + (bundle != null ? ", version " + bundle.getVersion()
235 + ", bundle " + bundle.getSymbolicName() : ""));
236 else
237 log.info("Data model "
238 + resUrl
239 + " updated from version "
240 + currentVersion
241 + (bundle != null ? ", version " + bundle.getVersion()
242 + ", bundle " + bundle.getSymbolicName() : ""));
243 } catch (Exception e) {
244 throw new ArgeoException("Cannot process data model " + resUrl, e);
245 } finally {
246 IOUtils.closeQuietly(reader);
247 }
248 }
249
250 protected byte[] readCndContent(String resUrl) {
251 InputStream in = null;
252 try {
253 boolean classpath;
254 // normalize URL
255 if (bundleContext != null && resUrl.startsWith("classpath:")) {
256 resUrl = resUrl.substring("classpath:".length());
257 classpath = true;
258 } else if (resUrl.indexOf(':') < 0) {
259 if (!resUrl.startsWith("/")) {
260 resUrl = "/" + resUrl;
261 log.warn("Classpath should start with '/'");
262 }
263 classpath = true;
264 } else {
265 classpath = false;
266 }
267
268 URL url = null;
269 if (classpath) {
270 if (bundleContext != null) {
271 Bundle currentBundle = bundleContext.getBundle();
272 url = currentBundle.getResource(resUrl);
273 } else {
274 resUrl = "classpath:" + resUrl;
275 url = null;
276 }
277 } else if (!resUrl.startsWith("classpath:")) {
278 url = new URL(resUrl);
279 }
280
281 if (url != null) {
282 in = url.openStream();
283 } else if (resourceLoader != null) {
284 Resource res = resourceLoader.getResource(resUrl);
285 in = res.getInputStream();
286 url = res.getURL();
287 } else {
288 throw new ArgeoException("No " + resUrl + " in the classpath,"
289 + " make sure the containing" + " package is visible.");
290 }
291
292 return IOUtils.toByteArray(in);
293 } catch (Exception e) {
294 throw new ArgeoException("Cannot read CND from " + resUrl, e);
295 } finally {
296 IOUtils.closeQuietly(in);
297 }
298 }
299
300 /*
301 * REPOSITORY INTERCEPTOR
302 */
303
304 /*
305 * UTILITIES
306 */
307 /** Find which OSGi bundle provided the data model resource */
308 protected Bundle findDataModelBundle(String resUrl) {
309 if (bundleContext == null)
310 return null;
311
312 if (resUrl.startsWith("/"))
313 resUrl = resUrl.substring(1);
314 String pkg = resUrl.substring(0, resUrl.lastIndexOf('/')).replace('/',
315 '.');
316 ServiceReference<PackageAdmin> paSr = bundleContext
317 .getServiceReference(PackageAdmin.class);
318 PackageAdmin packageAdmin = (PackageAdmin) bundleContext
319 .getService(paSr);
320
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;
331 break;
332 }
333 }
334 }
335
336 Bundle exportingBundle = null;
337 if (exportedPackage != null) {
338 exportingBundle = exportedPackage.getExportingBundle();
339 } else {
340 // assume this is in the same bundle
341 exportingBundle = bundleContext.getBundle();
342 // throw new ArgeoException("No OSGi exporting package found for "
343 // + resUrl);
344 }
345 return exportingBundle;
346 }
347
348 /*
349 * FIELDS ACCESS
350 */
351 public void setNamespaces(Map<String, String> namespaces) {
352 this.namespaces = namespaces;
353 }
354
355 public void setCndFiles(List<String> cndFiles) {
356 this.cndFiles = cndFiles;
357 }
358
359 public void setBundleContext(BundleContext bundleContext) {
360 this.bundleContext = bundleContext;
361 }
362
363 protected BundleContext getBundleContext() {
364 return bundleContext;
365 }
366
367 public void setForceCndImport(Boolean forceCndUpdate) {
368 this.forceCndImport = forceCndUpdate;
369 }
370
371 public void setResourceLoader(ResourceLoader resourceLoader) {
372 this.resourceLoader = resourceLoader;
373 }
374
375 public void setAdminCredentials(Credentials adminCredentials) {
376 this.adminCredentials = adminCredentials;
377 }
378
379 }