]> git.argeo.org Git - lgpl/argeo-commons.git/blob - JackrabbitWrapper.java
7288cdf4bbea30908e1f813210fe443442f4ff36
[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.LoginException;
30 import javax.jcr.NoSuchWorkspaceException;
31 import javax.jcr.Node;
32 import javax.jcr.NodeIterator;
33 import javax.jcr.RepositoryException;
34 import javax.jcr.Session;
35 import javax.jcr.nodetype.NodeType;
36
37 import org.apache.commons.io.FilenameUtils;
38 import org.apache.commons.io.IOUtils;
39 import org.apache.commons.logging.Log;
40 import org.apache.commons.logging.LogFactory;
41 import org.apache.jackrabbit.api.JackrabbitRepository;
42 import org.apache.jackrabbit.commons.NamespaceHelper;
43 import org.apache.jackrabbit.commons.cnd.CndImporter;
44 import org.argeo.ArgeoException;
45 import org.argeo.jcr.ArgeoJcrConstants;
46 import org.argeo.jcr.ArgeoNames;
47 import org.argeo.jcr.ArgeoTypes;
48 import org.argeo.jcr.JcrRepositoryWrapper;
49 import org.argeo.jcr.JcrUtils;
50 import org.argeo.util.security.DigestUtils;
51 import org.osgi.framework.Bundle;
52 import org.osgi.framework.BundleContext;
53 import org.osgi.framework.ServiceReference;
54 import org.osgi.service.packageadmin.ExportedPackage;
55 import org.osgi.service.packageadmin.PackageAdmin;
56 import org.springframework.context.ResourceLoaderAware;
57 import org.springframework.core.io.Resource;
58 import org.springframework.core.io.ResourceLoader;
59
60 /**
61 * Wrapper around a Jackrabbit repository which allows to simplify configuration
62 * and intercept some actions. It exposes itself as a {@link Repository}.
63 */
64 @SuppressWarnings("deprecation")
65 public class JackrabbitWrapper extends JcrRepositoryWrapper implements
66 JackrabbitRepository, ResourceLoaderAware {
67 private final static Log log = LogFactory.getLog(JackrabbitWrapper.class);
68 private final static String DIGEST_ALGORITHM = "MD5";
69
70 // local
71 private ResourceLoader resourceLoader;
72
73 // data model
74 /** Node type definitions in CND format */
75 private List<String> cndFiles = new ArrayList<String>();
76 /**
77 * Always import CNDs. Useful during development of new data models. In
78 * production, explicit migration processes should be used.
79 */
80 private Boolean forceCndImport = true;
81
82 /** Namespaces to register: key is prefix, value namespace */
83 private Map<String, String> namespaces = new HashMap<String, String>();
84
85 private BundleContext bundleContext;
86
87 /**
88 * Explicitly set admin credentials used in initialization. Useful for
89 * testing, in real applications authentication is rather dealt with
90 * externally
91 */
92 private Credentials adminCredentials = null;
93
94 /**
95 * Empty constructor, {@link #init()} should be called after properties have
96 * been set
97 */
98 public JackrabbitWrapper() {
99 }
100
101 @Override
102 public void init() {
103 prepareDataModel();
104 }
105
106 /*
107 * DATA MODEL
108 */
109
110 /**
111 * Import declared node type definitions and register namespaces. Tries to
112 * update the node definitions if they have changed. In case of failures an
113 * error will be logged but no exception will be thrown.
114 */
115 protected void prepareDataModel() {
116 if ((cndFiles == null || cndFiles.size() == 0)
117 && (namespaces == null || namespaces.size() == 0))
118 return;
119
120 Session session = null;
121 try {
122 session = login(adminCredentials);
123 // register namespaces
124 if (namespaces.size() > 0) {
125 NamespaceHelper namespaceHelper = new NamespaceHelper(session);
126 namespaceHelper.registerNamespaces(namespaces);
127 }
128
129 // load CND files from classpath or as URL
130 for (String resUrl : cndFiles) {
131 processCndFile(session, resUrl);
132 }
133 } catch (Exception e) {
134 JcrUtils.discardQuietly(session);
135 throw new ArgeoException("Cannot import node type definitions "
136 + cndFiles, e);
137 } finally {
138 JcrUtils.logoutQuietly(session);
139 }
140
141 }
142
143 protected void processCndFile(Session session, String resUrl) {
144 Reader reader = null;
145 try {
146 // check existing data model nodes
147 new NamespaceHelper(session).registerNamespace(ArgeoNames.ARGEO,
148 ArgeoNames.ARGEO_NAMESPACE);
149 if (!session.itemExists(ArgeoJcrConstants.DATA_MODELS_BASE_PATH))
150 JcrUtils.mkdirs(session,
151 ArgeoJcrConstants.DATA_MODELS_BASE_PATH);
152 Node dataModels = session
153 .getNode(ArgeoJcrConstants.DATA_MODELS_BASE_PATH);
154 NodeIterator it = dataModels.getNodes();
155 Node dataModel = null;
156 while (it.hasNext()) {
157 Node node = it.nextNode();
158 if (node.getProperty(ArgeoNames.ARGEO_URI).getString()
159 .equals(resUrl)) {
160 dataModel = node;
161 break;
162 }
163 }
164
165 byte[] cndContent = readCndContent(resUrl);
166 String newDigest = DigestUtils.digest(DIGEST_ALGORITHM, cndContent);
167 Bundle bundle = findDataModelBundle(resUrl);
168
169 String currentVersion = null;
170 if (dataModel != null) {
171 currentVersion = dataModel.getProperty(
172 ArgeoNames.ARGEO_DATA_MODEL_VERSION).getString();
173 if (dataModel.hasNode(Node.JCR_CONTENT)) {
174 String oldDigest = JcrUtils.checksumFile(dataModel,
175 DIGEST_ALGORITHM);
176 if (oldDigest.equals(newDigest)) {
177 if (log.isTraceEnabled())
178 log.trace("Data model " + resUrl
179 + " hasn't changed, keeping version "
180 + currentVersion);
181 return;
182 }
183 }
184 }
185
186 if (dataModel != null && !forceCndImport) {
187 log.info("Data model "
188 + resUrl
189 + " has changed since version "
190 + currentVersion
191 + (bundle != null ? ": version " + bundle.getVersion()
192 + ", bundle " + bundle.getSymbolicName() : ""));
193 return;
194 }
195
196 reader = new InputStreamReader(new ByteArrayInputStream(cndContent));
197 // actually imports the CND
198 try {
199 CndImporter.registerNodeTypes(reader, session, true);
200 } catch (Exception e) {
201 log.error("Cannot import data model " + resUrl, e);
202 return;
203 }
204
205 if (dataModel != null && !dataModel.isNodeType(NodeType.NT_FILE)) {
206 dataModel.remove();
207 dataModel = null;
208 }
209
210 // FIXME: what if argeo.cnd would not be the first called on
211 // a new repo? argeo:dataModel would not be found
212 String fileName = FilenameUtils.getName(resUrl);
213 if (dataModel == null) {
214 dataModel = dataModels.addNode(fileName, NodeType.NT_FILE);
215 dataModel.addNode(Node.JCR_CONTENT, NodeType.NT_RESOURCE);
216 dataModel.addMixin(ArgeoTypes.ARGEO_DATA_MODEL);
217 dataModel.setProperty(ArgeoNames.ARGEO_URI, resUrl);
218 } else {
219 session.getWorkspace().getVersionManager()
220 .checkout(dataModel.getPath());
221 }
222 if (bundle != null)
223 dataModel.setProperty(ArgeoNames.ARGEO_DATA_MODEL_VERSION,
224 bundle.getVersion().toString());
225 else
226 dataModel.setProperty(ArgeoNames.ARGEO_DATA_MODEL_VERSION,
227 "0.0.0");
228 JcrUtils.copyBytesAsFile(dataModel.getParent(), fileName,
229 cndContent);
230 JcrUtils.updateLastModified(dataModel);
231 session.save();
232 session.getWorkspace().getVersionManager()
233 .checkin(dataModel.getPath());
234
235 if (currentVersion == null)
236 log.info("Data model "
237 + resUrl
238 + (bundle != null ? ", version " + bundle.getVersion()
239 + ", bundle " + bundle.getSymbolicName() : ""));
240 else
241 log.info("Data model "
242 + resUrl
243 + " updated from version "
244 + currentVersion
245 + (bundle != null ? ", version " + bundle.getVersion()
246 + ", bundle " + bundle.getSymbolicName() : ""));
247 } catch (Exception e) {
248 throw new ArgeoException("Cannot process data model " + resUrl, e);
249 } finally {
250 IOUtils.closeQuietly(reader);
251 }
252 }
253
254 protected byte[] readCndContent(String resUrl) {
255 InputStream in = null;
256 try {
257 boolean classpath;
258 // normalize URL
259 if (bundleContext != null && resUrl.startsWith("classpath:")) {
260 resUrl = resUrl.substring("classpath:".length());
261 classpath = true;
262 } else if (resUrl.indexOf(':') < 0) {
263 if (!resUrl.startsWith("/")) {
264 resUrl = "/" + resUrl;
265 log.warn("Classpath should start with '/'");
266 }
267 classpath = true;
268 } else {
269 classpath = false;
270 }
271
272 URL url = null;
273 if (classpath) {
274 if (bundleContext != null) {
275 Bundle currentBundle = bundleContext.getBundle();
276 url = currentBundle.getResource(resUrl);
277 } else {
278 resUrl = "classpath:" + resUrl;
279 url = null;
280 }
281 } else if (!resUrl.startsWith("classpath:")) {
282 url = new URL(resUrl);
283 }
284
285 if (url != null) {
286 in = url.openStream();
287 } else if (resourceLoader != null) {
288 Resource res = resourceLoader.getResource(resUrl);
289 in = res.getInputStream();
290 url = res.getURL();
291 } else {
292 throw new ArgeoException("No " + resUrl + " in the classpath,"
293 + " make sure the containing" + " package is visible.");
294 }
295
296 return IOUtils.toByteArray(in);
297 } catch (Exception e) {
298 throw new ArgeoException("Cannot read CND from " + resUrl, e);
299 } finally {
300 IOUtils.closeQuietly(in);
301 }
302 }
303
304 /*
305 * JACKRABBIT REPOSITORY IMPLEMENTATION
306 */
307 @Override
308 public Session login(Credentials credentials, String workspaceName,
309 Map<String, Object> attributes) throws LoginException,
310 NoSuchWorkspaceException, RepositoryException {
311 // TODO Auto-generated method stub
312 return null;
313 }
314
315 @Override
316 public void shutdown() {
317 // TODO Auto-generated method stub
318
319 }
320
321 /*
322 * UTILITIES
323 */
324 /** Find which OSGi bundle provided the data model resource */
325 protected Bundle findDataModelBundle(String resUrl) {
326 if (bundleContext == null)
327 return null;
328
329 if (resUrl.startsWith("/"))
330 resUrl = resUrl.substring(1);
331 String pkg = resUrl.substring(0, resUrl.lastIndexOf('/')).replace('/',
332 '.');
333 ServiceReference<PackageAdmin> paSr = bundleContext
334 .getServiceReference(PackageAdmin.class);
335 PackageAdmin packageAdmin = (PackageAdmin) bundleContext
336 .getService(paSr);
337
338 // find exported package
339 ExportedPackage exportedPackage = null;
340 ExportedPackage[] exportedPackages = packageAdmin
341 .getExportedPackages(pkg);
342 if (exportedPackages == null)
343 throw new ArgeoException("No exported package found for " + pkg);
344 for (ExportedPackage ep : exportedPackages) {
345 for (Bundle b : ep.getImportingBundles()) {
346 if (b.getBundleId() == bundleContext.getBundle().getBundleId()) {
347 exportedPackage = ep;
348 break;
349 }
350 }
351 }
352
353 Bundle exportingBundle = null;
354 if (exportedPackage != null) {
355 exportingBundle = exportedPackage.getExportingBundle();
356 } else {
357 // assume this is in the same bundle
358 exportingBundle = bundleContext.getBundle();
359 // throw new ArgeoException("No OSGi exporting package found for "
360 // + resUrl);
361 }
362 return exportingBundle;
363 }
364
365 /*
366 * FIELDS ACCESS
367 */
368 public void setNamespaces(Map<String, String> namespaces) {
369 this.namespaces = namespaces;
370 }
371
372 public void setCndFiles(List<String> cndFiles) {
373 this.cndFiles = cndFiles;
374 }
375
376 public void setBundleContext(BundleContext bundleContext) {
377 this.bundleContext = bundleContext;
378 }
379
380 protected BundleContext getBundleContext() {
381 return bundleContext;
382 }
383
384 public void setForceCndImport(Boolean forceCndUpdate) {
385 this.forceCndImport = forceCndUpdate;
386 }
387
388 public void setResourceLoader(ResourceLoader resourceLoader) {
389 this.resourceLoader = resourceLoader;
390 }
391
392 public void setAdminCredentials(Credentials adminCredentials) {
393 this.adminCredentials = adminCredentials;
394 }
395
396 }