View Javadoc
1   /**
2    * This file Copyright (c) 2003-2015 Magnolia International
3    * Ltd.  (http://www.magnolia-cms.com). All rights reserved.
4    *
5    *
6    * This file is dual-licensed under both the Magnolia
7    * Network Agreement and the GNU General Public License.
8    * You may elect to use one or the other of these licenses.
9    *
10   * This file is distributed in the hope that it will be
11   * useful, but AS-IS and WITHOUT ANY WARRANTY; without even the
12   * implied warranty of MERCHANTABILITY or FITNESS FOR A
13   * PARTICULAR PURPOSE, TITLE, or NONINFRINGEMENT.
14   * Redistribution, except as permitted by whichever of the GPL
15   * or MNA you select, is prohibited.
16   *
17   * 1. For the GPL license (GPL), you can redistribute and/or
18   * modify this file under the terms of the GNU General
19   * Public License, Version 3, as published by the Free Software
20   * Foundation.  You should have received a copy of the GNU
21   * General Public License, Version 3 along with this program;
22   * if not, write to the Free Software Foundation, Inc., 51
23   * Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
24   *
25   * 2. For the Magnolia Network Agreement (MNA), this file
26   * and the accompanying materials are made available under the
27   * terms of the MNA which accompanies this distribution, and
28   * is available at http://www.magnolia-cms.com/mna.html
29   *
30   * Any modifications to this file must keep this entire header
31   * intact.
32   *
33   */
34  package info.magnolia.jackrabbit;
35  
36  import info.magnolia.cms.beans.config.ContentRepository;
37  import info.magnolia.cms.core.Path;
38  import info.magnolia.cms.core.SystemProperty;
39  import info.magnolia.repository.Provider;
40  import info.magnolia.repository.RepositoryNotInitializedException;
41  import info.magnolia.repository.definition.RepositoryDefinition;
42  
43  import java.io.File;
44  import java.io.FileInputStream;
45  import java.io.FileNotFoundException;
46  import java.io.IOException;
47  import java.io.InputStream;
48  import java.lang.reflect.InvocationTargetException;
49  import java.lang.reflect.Method;
50  import java.util.Hashtable;
51  import java.util.Iterator;
52  import java.util.Map;
53  
54  import javax.jcr.NamespaceException;
55  import javax.jcr.Repository;
56  import javax.jcr.RepositoryException;
57  import javax.jcr.Session;
58  import javax.jcr.SimpleCredentials;
59  import javax.jcr.Workspace;
60  import javax.jcr.nodetype.NoSuchNodeTypeException;
61  import javax.jcr.nodetype.NodeTypeManager;
62  import javax.naming.Context;
63  import javax.naming.InitialContext;
64  import javax.naming.NameNotFoundException;
65  import javax.naming.NamingException;
66  import javax.xml.transform.TransformerFactoryConfigurationError;
67  
68  import org.apache.commons.beanutils.PropertyUtils;
69  import org.apache.commons.io.IOUtils;
70  import org.apache.commons.lang3.ArrayUtils;
71  import org.apache.commons.lang3.JavaVersion;
72  import org.apache.commons.lang3.StringUtils;
73  import org.apache.commons.lang3.SystemUtils;
74  import org.apache.jackrabbit.core.WorkspaceImpl;
75  import org.apache.jackrabbit.core.jndi.RegistryHelper;
76  import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl;
77  import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
78  import org.apache.jackrabbit.core.nodetype.xml.NodeTypeReader;
79  import org.apache.jackrabbit.spi.Name;
80  import org.slf4j.Logger;
81  import org.slf4j.LoggerFactory;
82  
83  
84  /**
85   * Provider implementation for Apache JackRabbit JCR repository.
86   */
87  public class ProviderImpl implements Provider {
88  
89      /**
90       * Magnolia property (entry in magnolia.properties) with the cluster id, which will be passed to jackrabbit.
91       */
92      private static final String MAGNOLIA_CLUSTERID_PROPERTY = "magnolia.clusterid";
93  
94      /**
95       * Jackrabbit system property for cluster node id.
96       */
97      private static final String JACKRABBIT_CLUSTER_ID_PROPERTY = "org.apache.jackrabbit.core.cluster.node_id";
98  
99      private static final Logger log = LoggerFactory.getLogger(ProviderImpl.class);
100 
101     private static final String CONFIG_FILENAME_KEY = "configFile";
102 
103     private static final String REPOSITORY_HOME_KEY = "repositoryHome";
104 
105     private static final String CONTEXT_FACTORY_CLASS_KEY = "contextFactoryClass";
106 
107     private static final String PROVIDER_URL_KEY = "providerURL";
108 
109     private static final String BIND_NAME_KEY = "bindName";
110 
111     private static final String MGNL_NODETYPES = "/mgnl-nodetypes/magnolia-nodetypes.xml";
112 
113     private static final String CUSTOM_NODETYPES = "customNodeTypes";
114 
115     private RepositoryDefinition repositoryMapping;
116 
117     private Repository repository;
118 
119     private String bindName;
120 
121     private Hashtable<String, Object> jndiEnv;
122 
123     private static final String REPO_HOME_PREFIX = "${repository.home}";
124 
125     private static final int REPO_HOME_SUFIX_LEN = REPO_HOME_PREFIX.length();
126 
127     private static final String sysRepositoryHome = System.getProperty("repository.home");
128 
129     private static final String sysRepositoryHomes = System.getProperty("repository.homes");
130 
131     /**
132      * Finds the physical path to the repository folder.
133      *
134      * @param repositoryHome the property set in the repository.xml file
135      * @return the full path resolved to the repository dir
136      */
137     private String getRepositoryHome(final String repositoryHome) {
138         boolean relocate = false;
139         String tmp = repositoryHome;
140         if (repositoryHome.startsWith(REPOSITORY_HOME_KEY)) {
141             tmp = repositoryHome.substring(REPO_HOME_SUFIX_LEN);
142             relocate = true;
143         }
144         /*
145          * Resolve if the path started with the suffix
146          */
147         if (sysRepositoryHome != null && relocate) {
148             return sysRepositoryHome + File.separator + tmp;
149         }
150 
151         /*
152          * This is apply to all repositories if the java property is set
153          */
154         if (sysRepositoryHomes != null) {
155             return sysRepositoryHomes + File.separator + tmp;
156         }
157 
158         /*
159          * Return the same value as before if neither of the above applied
160          */
161         return Path.getAbsoluteFileSystemPath(tmp);
162     }
163 
164     /**
165      * @see info.magnolia.repository.Provider#init(info.magnolia.repository.definition.RepositoryDefinition)
166      */
167     @Override
168     public void init(RepositoryDefinition repositoryMapping) throws RepositoryNotInitializedException {
169         checkXmlSettings();
170 
171         this.repositoryMapping = repositoryMapping;
172         /* connect to repository */
173         Map params = this.repositoryMapping.getParameters();
174         String configFile = (String) params.get(CONFIG_FILENAME_KEY);
175         configFile = Path.getAbsoluteFileSystemPath(configFile);
176         String repositoryHome = (String) params.get(REPOSITORY_HOME_KEY);
177         repositoryHome = getRepositoryHome(repositoryHome);
178 
179         // cleanup the path, to remove eventual ../.. and make it absolute
180         try {
181             File repoHomeFile = new File(repositoryHome);
182             repositoryHome = repoHomeFile.getCanonicalPath();
183         } catch (IOException e1) {
184             // should never happen and it's not a problem at this point, just pass it to jackrabbit and see
185         }
186 
187         String clusterid = SystemProperty.getProperty(MAGNOLIA_CLUSTERID_PROPERTY);
188         if (StringUtils.isNotBlank(clusterid)) {
189             System.setProperty(JACKRABBIT_CLUSTER_ID_PROPERTY, clusterid);
190         }
191 
192         // get it back from system properties, if it has been set elsewhere
193         clusterid = System.getProperty(JACKRABBIT_CLUSTER_ID_PROPERTY);
194 
195         log.info("Loading repository at {} (config file: {}) - cluster id: \"{}\"", repositoryHome, configFile, StringUtils.defaultString(clusterid, "<unset>"));
196 
197         bindName = (String) params.get(BIND_NAME_KEY);
198         jndiEnv = new Hashtable<String, Object>();
199         jndiEnv.put(Context.INITIAL_CONTEXT_FACTORY, params.get(CONTEXT_FACTORY_CLASS_KEY));
200         jndiEnv.put(Context.PROVIDER_URL, params.get(PROVIDER_URL_KEY));
201 
202         try {
203             InitialContext ctx = new InitialContext(jndiEnv);
204             // first try to find the existing object if any
205             try {
206                 this.repository = (Repository) ctx.lookup(bindName);
207             } catch (NameNotFoundException ne) {
208                 log.debug("No JNDI bound Repository found with name {}, trying to initialize a new Repository", bindName);
209                 RegistryHelper.registerRepository(ctx, bindName, configFile, repositoryHome, true);
210                 this.repository = (Repository) ctx.lookup(bindName);
211             }
212             this.validateWorkspaces();
213         } catch (NamingException e) {
214             log.error("Unable to initialize repository: {}", e.getMessage(), e);
215             throw new RepositoryNotInitializedException(e);
216         } catch (RepositoryException e) {
217             log.error("Unable to initialize repository: {}", e.getMessage(), e);
218             throw new RepositoryNotInitializedException(e);
219         } catch (TransformerFactoryConfigurationError e) {
220             log.error("Unable to initialize repository: {}", e.getMessage(), e);
221             throw new RepositoryNotInitializedException(e);
222         }
223     }
224 
225     @Override
226     public void shutdownRepository() {
227         log.info("Shutting down repository bound to '{}'", bindName);
228 
229         try {
230             Context ctx = new InitialContext(jndiEnv);
231             RegistryHelper.unregisterRepository(ctx, bindName);
232         } catch (NamingException e) {
233             log.warn("Unable to shutdown repository {}: {}", bindName, e.getMessage(), e);
234         } catch (Throwable e) {
235             log.error("Failed to shutdown repository {}: {}", bindName, e.getMessage(), e);
236         }
237     }
238 
239     @Override
240     public Repository getUnderlyingRepository() throws RepositoryNotInitializedException {
241         if (this.repository == null) {
242             throw new RepositoryNotInitializedException("Null repository");
243         }
244         return this.repository;
245     }
246 
247     /**
248      * @see info.magnolia.repository.Provider#registerNamespace(java.lang.String, java.lang.String, javax.jcr.Workspace)
249      */
250     @Override
251     public void registerNamespace(String namespacePrefix, String uri, Workspace workspace) throws RepositoryException {
252         try {
253             workspace.getNamespaceRegistry().getURI(namespacePrefix);
254         } catch (NamespaceException e) {
255             log.debug(e.getMessage());
256 
257             log.info("Registering prefix [{}] with URI {}", namespacePrefix, uri);
258             workspace.getNamespaceRegistry().registerNamespace(namespacePrefix, uri);
259         }
260     }
261 
262     /**
263      * @see info.magnolia.repository.Provider#unregisterNamespace(java.lang.String, javax.jcr.Workspace)
264      */
265     @Override
266     public void unregisterNamespace(String prefix, Workspace workspace) throws RepositoryException {
267         workspace.getNamespaceRegistry().unregisterNamespace(prefix);
268     }
269 
270     /**
271      * @see info.magnolia.repository.Provider#registerNodeTypes(String)
272      */
273     @Override
274     public void registerNodeTypes() throws RepositoryException {
275         registerNodeTypes(StringUtils.EMPTY);
276     }
277 
278     /**
279      * @see info.magnolia.repository.Provider#registerNodeTypes(java.lang.String)
280      */
281     @Override
282     public void registerNodeTypes(String configuration) throws RepositoryException {
283         if (StringUtils.isEmpty(configuration)) {
284             configuration = this.repositoryMapping.getParameters().get(CUSTOM_NODETYPES);
285         }
286 
287         InputStream xml = getNodeTypeDefinition(configuration);
288         this.registerNodeTypes(xml);
289     }
290 
291     /**
292      * @see info.magnolia.repository.Provider#registerNodeTypes(java.io.InputStream)
293      */
294     @Override
295     public void registerNodeTypes(InputStream xmlStream) throws RepositoryException {
296         SimpleCredentials credentials = new SimpleCredentials(
297                 ContentRepository.REPOSITORY_USER,
298                 ContentRepository.REPOSITORY_PSWD.toCharArray());
299         Session jcrSession = this.repository.login(credentials);
300 
301         try {
302 
303             Workspace workspace = jcrSession.getWorkspace();
304 
305             // should never happen
306             if (xmlStream == null) {
307                 throw new MissingNodetypesException();
308             }
309 
310             // Use Objects so that it works both with jackrabbit 1.x (NodeTypeDef) and jackrabbit 2
311             // (QNodeTypeDefinition)
312             Object[] types;
313 
314             try {
315                 types = (Object[]) NodeTypeReader.class.getMethod("read", new Class[]{InputStream.class}).invoke(null, xmlStream);
316             } catch (Exception e) {
317                 throw new RepositoryException(e.getMessage(), e);
318             } finally {
319                 IOUtils.closeQuietly(xmlStream);
320             }
321 
322             NodeTypeManager ntMgr = workspace.getNodeTypeManager();
323             NodeTypeRegistry ntReg;
324             try {
325                 ntReg = ((NodeTypeManagerImpl) ntMgr).getNodeTypeRegistry();
326             } catch (ClassCastException e) {
327                 // this could happen if the repository provider does not have proper Shared API for the
328                 // application server like at the moment in Jackrabbit
329                 log.debug("Failed to get NodeTypeRegistry: ", e);
330                 return;
331             }
332 
333             for (int j = 0; j < types.length; j++) {
334                 Object def = types[j];
335 
336                 Name ntname;
337                 try {
338                     ntname = (Name) PropertyUtils.getProperty(def, "name");
339                 } catch (Exception e) {
340                     throw new RepositoryException(e.getMessage(), e);
341                 }
342 
343                 try {
344 
345                     // return value has changed in jackrabbit 2, we still have to use reflection here
346                     // ntReg.getNodeTypeDef(ntname);
347 
348                     Method method = ntReg.getClass().getMethod("getNodeTypeDef", Name.class);
349                     method.invoke(ntReg, ntname);
350                 } catch (IllegalArgumentException e) {
351                     throw new RepositoryException(e.getMessage(), e);
352                 } catch (IllegalAccessException e) {
353                     throw new RepositoryException(e.getMessage(), e);
354                 } catch (SecurityException e) {
355                     throw new RepositoryException(e.getMessage(), e);
356                 } catch (NoSuchMethodException e) {
357                     throw new RepositoryException(e.getMessage(), e);
358                 } catch (InvocationTargetException ite) {
359                     if (ite.getTargetException() instanceof NoSuchNodeTypeException) {
360                         log.info("Registering nodetype {} on repository {}", ntname, repositoryMapping.getName());
361 
362                         try {
363                             // reflection for jackrabbit 1+2 compatibility
364                             getMethod(NodeTypeRegistry.class, "registerNodeType").invoke(ntReg, def);
365                         } catch (Exception e) {
366                             throw new RepositoryException(e.getMessage(), e);
367                         }
368                     }
369                 }
370             }
371 
372         } finally {
373             jcrSession.logout();
374         }
375     }
376 
377     private Method getMethod(Class theclass, String methodName) throws NoSuchMethodException {
378         Method[] declaredMethods = theclass.getDeclaredMethods();
379 
380         for (Method method : declaredMethods) {
381             if (method.getName().equals(methodName)) {
382                 return method;
383             }
384         }
385 
386         throw new NoSuchMethodException(theclass.getName() + "." + methodName + "()");
387     }
388 
389     /**
390      * @return InputStream of node type definition file
391      */
392     private InputStream getNodeTypeDefinition(String configuration) {
393 
394         InputStream xml;
395 
396         if (StringUtils.isNotEmpty(configuration)) {
397 
398             // 1: try to load the configured file from the classpath
399             xml = getClass().getResourceAsStream(configuration);
400             if (xml != null) {
401                 log.info("Custom node types registered using {}", configuration);
402                 return xml;
403             }
404 
405             // 2: try to load it from the file system
406             File nodeTypeDefinition = new File(Path.getAbsoluteFileSystemPath(configuration));
407             if (nodeTypeDefinition.exists()) {
408                 try {
409                     return new FileInputStream(nodeTypeDefinition);
410                 } catch (FileNotFoundException e) {
411                     // should never happen
412                     log.error("File not found: {}", configuration);
413                 }
414             }
415 
416             // 3: defaults to standard nodetypes
417             log.error("Unable to find node type definition: {} for repository {}", configuration, this.repositoryMapping.getName());
418         }
419 
420         // initialize default magnolia nodetypes
421         xml = getClass().getResourceAsStream(MGNL_NODETYPES);
422 
423         return xml;
424     }
425 
426     /**
427      * WORKAROUND for tomcat 5.0/jdk 1.5 problem tomcat\common\endorsed contains an xml-apis.jar needed by tomcat and
428      * loaded before all xmsl stuff present in the jdk (1.4 naming problem). In the xml-apis.jar file the
429      * TransformerFactoryImpl is set to "org.apache.xalan.processor.TransformerFactoryImpl" instead of
430      * "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl". solution: remove the file xml-apis.jar
431      * from the directory OR manually change the javax.xml.transform.TransformerFactory system property
432      */
433     protected void checkXmlSettings() {
434         if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_1_5)
435                 && "org.apache.xalan.processor.TransformerFactoryImpl".equals(System
436                 .getProperty("javax.xml.transform.TransformerFactory"))) {
437 
438             String transformerClass = "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl";
439 
440             try {
441                 Class.forName(transformerClass);
442 
443                 System.setProperty("javax.xml.transform.TransformerFactory", transformerClass);
444 
445                 log.info("Java 1.5 detected, setting system property \"javax.xml.transform.TransformerFactory\" to \"{}\"", transformerClass);
446             } catch (Throwable e) {
447                 // not in the classpath. We can't assume which one to use, so just go on
448             }
449         }
450     }
451 
452     /**
453      * Checks if all workspaces are present according to the repository mapping, creates any missing workspace.
454      */
455     private void validateWorkspaces() throws RepositoryException {
456         Iterator<String> configuredNames = repositoryMapping.getWorkspaces().iterator();
457         while (configuredNames.hasNext()) {
458             registerWorkspace(configuredNames.next());
459         }
460     }
461 
462     /**
463      * @see info.magnolia.repository.Provider#registerWorkspace(java.lang.String)
464      */
465     @Override
466     public boolean registerWorkspace(String workspaceName) throws RepositoryException {
467         // check if workspace already exists
468         SimpleCredentials credentials = new SimpleCredentials(
469                 ContentRepository.REPOSITORY_USER,
470                 ContentRepository.REPOSITORY_PSWD.toCharArray());
471         Session jcrSession = this.repository.login(credentials);
472 
473         try {
474             WorkspaceImpl defaultWorkspace = (WorkspaceImpl) jcrSession.getWorkspace();
475             String[] workspaceNames = defaultWorkspace.getAccessibleWorkspaceNames();
476 
477             boolean alreadyExists = ArrayUtils.contains(workspaceNames, workspaceName);
478             if (!alreadyExists) {
479                 defaultWorkspace.createWorkspace(workspaceName);
480             }
481             jcrSession.logout();
482 
483             return !alreadyExists;
484         } catch (ClassCastException e) {
485             // this could happen if the repository provider does not have proper Shared API for the
486             // application server like at the moment in Jackrabbit
487             log.debug("Unable to register workspace, will continue", e);
488         } catch (Throwable t) {
489             log.error("Unable to register workspace, will continue", t);
490         }
491         return false;
492     }
493 
494     @Override
495     public Session getSystemSession(String workspaceName) throws RepositoryException {
496 
497         // FIXME: stop using SystemProperty, but IoC is not ready yet when this is called (config loader calls repo.init() which results in authentication calls being made and this method being invoked
498 
499         String user = SystemProperty.getProperty("magnolia.connection.jcr.admin.userId", SystemProperty.getProperty("magnolia.connection.jcr.userId", "admin"));
500         String pwd = SystemProperty.getProperty("magnolia.connection.jcr.admin.password", SystemProperty.getProperty("magnolia.connection.jcr.password", "admin"));
501         return this.repository.login(new SimpleCredentials(user, pwd.toCharArray()), workspaceName);
502     }
503 
504 }