]> git.argeo.org Git - lgpl/argeo-commons.git/blob - server/runtime/org.argeo.server.jackrabbit/src/main/java/org/argeo/jackrabbit/JackrabbitWrapper.java
7ed7737f8ca8806fa0296eef64f7c8d49206419a
[lgpl/argeo-commons.git] / server / runtime / org.argeo.server.jackrabbit / src / main / java / org / argeo / jackrabbit / 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.Repository;
32 import javax.jcr.Session;
33 import javax.jcr.nodetype.NodeType;
34
35 import org.apache.commons.io.FilenameUtils;
36 import org.apache.commons.io.IOUtils;
37 import org.apache.commons.logging.Log;
38 import org.apache.commons.logging.LogFactory;
39 import org.apache.jackrabbit.commons.NamespaceHelper;
40 import org.apache.jackrabbit.commons.cnd.CndImporter;
41 import org.argeo.ArgeoException;
42 import org.argeo.jcr.ArgeoJcrConstants;
43 import org.argeo.jcr.ArgeoNames;
44 import org.argeo.jcr.ArgeoTypes;
45 import org.argeo.jcr.JcrRepositoryWrapper;
46 import org.argeo.jcr.JcrUtils;
47 import org.argeo.util.security.DigestUtils;
48 import org.osgi.framework.Bundle;
49 import org.osgi.framework.BundleContext;
50 import org.osgi.framework.ServiceReference;
51 import org.osgi.service.packageadmin.ExportedPackage;
52 import org.osgi.service.packageadmin.PackageAdmin;
53 import org.springframework.context.ResourceLoaderAware;
54 import org.springframework.core.io.Resource;
55 import org.springframework.core.io.ResourceLoader;
56
57 /**
58 * Wrapper around a Jackrabbit repository which allows to simplify configuration
59 * and intercept some actions. It exposes itself as a {@link Repository}.
60 */
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.isDebugEnabled())
174 log.debug("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 CndImporter.registerNodeTypes(reader, session, true);
195
196 if (dataModel != null && !dataModel.isNodeType(NodeType.NT_FILE)) {
197 dataModel.remove();
198 dataModel = null;
199 }
200
201 // FIXME: what if argeo.cnd would not be the first called on
202 // a new repo? argeo:dataModel would not be found
203 String fileName = FilenameUtils.getName(resUrl);
204 if (dataModel == null) {
205 dataModel = dataModels.addNode(fileName, NodeType.NT_FILE);
206 dataModel.addNode(Node.JCR_CONTENT, NodeType.NT_RESOURCE);
207 dataModel.addMixin(ArgeoTypes.ARGEO_DATA_MODEL);
208 dataModel.setProperty(ArgeoNames.ARGEO_URI, resUrl);
209 } else {
210 session.getWorkspace().getVersionManager()
211 .checkout(dataModel.getPath());
212 }
213 if (bundle != null)
214 dataModel.setProperty(ArgeoNames.ARGEO_DATA_MODEL_VERSION,
215 bundle.getVersion().toString());
216 else
217 dataModel.setProperty(ArgeoNames.ARGEO_DATA_MODEL_VERSION,
218 "0.0.0");
219 JcrUtils.copyBytesAsFile(dataModel.getParent(), fileName,
220 cndContent);
221 JcrUtils.updateLastModified(dataModel);
222 session.save();
223 session.getWorkspace().getVersionManager()
224 .checkin(dataModel.getPath());
225
226 if (currentVersion == null)
227 log.info("Data model "
228 + resUrl
229 + (bundle != null ? ", version " + bundle.getVersion()
230 + ", bundle " + bundle.getSymbolicName() : ""));
231 else
232 log.info("Data model "
233 + resUrl
234 + " updated from version "
235 + currentVersion
236 + (bundle != null ? ", version " + bundle.getVersion()
237 + ", bundle " + bundle.getSymbolicName() : ""));
238 } catch (Exception e) {
239 throw new ArgeoException("Cannot process data model " + resUrl, e);
240 } finally {
241 IOUtils.closeQuietly(reader);
242 }
243 }
244
245 protected byte[] readCndContent(String resUrl) {
246 InputStream in = null;
247 try {
248 boolean classpath;
249 // normalize URL
250 if (bundleContext != null && resUrl.startsWith("classpath:")) {
251 resUrl = resUrl.substring("classpath:".length());
252 classpath = true;
253 } else if (resUrl.indexOf(':') < 0) {
254 if (!resUrl.startsWith("/")) {
255 resUrl = "/" + resUrl;
256 log.warn("Classpath should start with '/'");
257 }
258 classpath = true;
259 } else {
260 classpath = false;
261 }
262
263 URL url = null;
264 if (classpath) {
265 if (bundleContext != null) {
266 Bundle currentBundle = bundleContext.getBundle();
267 url = currentBundle.getResource(resUrl);
268 } else {
269 resUrl = "classpath:" + resUrl;
270 url = null;
271 }
272 } else if (!resUrl.startsWith("classpath:")) {
273 url = new URL(resUrl);
274 }
275
276 if (url != null) {
277 in = url.openStream();
278 } else if (resourceLoader != null) {
279 Resource res = resourceLoader.getResource(resUrl);
280 in = res.getInputStream();
281 url = res.getURL();
282 } else {
283 throw new ArgeoException("No " + resUrl + " in the classpath,"
284 + " make sure the containing" + " package is visible.");
285 }
286
287 return IOUtils.toByteArray(in);
288 } catch (Exception e) {
289 throw new ArgeoException("Cannot read CND from " + resUrl, e);
290 } finally {
291 IOUtils.closeQuietly(in);
292 }
293 }
294
295 /*
296 * REPOSITORY INTERCEPTOR
297 */
298
299 /*
300 * UTILITIES
301 */
302 /** Find which OSGi bundle provided the data model resource */
303 protected Bundle findDataModelBundle(String resUrl) {
304 if (bundleContext == null)
305 return null;
306
307 if (resUrl.startsWith("/"))
308 resUrl = resUrl.substring(1);
309 String pkg = resUrl.substring(0, resUrl.lastIndexOf('/')).replace('/',
310 '.');
311 ServiceReference paSr = bundleContext
312 .getServiceReference(PackageAdmin.class.getName());
313 PackageAdmin packageAdmin = (PackageAdmin) bundleContext
314 .getService(paSr);
315
316 // find exported package
317 ExportedPackage exportedPackage = null;
318 ExportedPackage[] exportedPackages = packageAdmin
319 .getExportedPackages(pkg);
320 if (exportedPackages == null)
321 throw new ArgeoException("No exported package found for " + pkg);
322 for (ExportedPackage ep : exportedPackages) {
323 for (Bundle b : ep.getImportingBundles()) {
324 if (b.getBundleId() == bundleContext.getBundle().getBundleId()) {
325 exportedPackage = ep;
326 break;
327 }
328 }
329 }
330
331 Bundle exportingBundle = null;
332 if (exportedPackage != null) {
333 exportingBundle = exportedPackage.getExportingBundle();
334 } else {
335 throw new ArgeoException("No OSGi exporting package found for "
336 + resUrl);
337 }
338 return exportingBundle;
339 }
340
341 /*
342 * FIELDS ACCESS
343 */
344 public void setNamespaces(Map<String, String> namespaces) {
345 this.namespaces = namespaces;
346 }
347
348 public void setCndFiles(List<String> cndFiles) {
349 this.cndFiles = cndFiles;
350 }
351
352 public void setBundleContext(BundleContext bundleContext) {
353 this.bundleContext = bundleContext;
354 }
355
356 public void setForceCndImport(Boolean forceCndUpdate) {
357 this.forceCndImport = forceCndUpdate;
358 }
359
360 public void setResourceLoader(ResourceLoader resourceLoader) {
361 this.resourceLoader = resourceLoader;
362 }
363
364 public void setAdminCredentials(Credentials adminCredentials) {
365 this.adminCredentials = adminCredentials;
366 }
367
368 }