View Javadoc

1   /**
2    * This file Copyright (c) 2003-2013 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.lang.ArrayUtils;
71  import org.apache.commons.lang.StringUtils;
72  import org.apache.commons.lang.SystemUtils;
73  import org.apache.jackrabbit.core.WorkspaceImpl;
74  import org.apache.jackrabbit.core.jndi.RegistryHelper;
75  import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl;
76  import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
77  import org.apache.jackrabbit.core.nodetype.xml.NodeTypeReader;
78  import org.apache.jackrabbit.spi.Name;
79  import org.slf4j.Logger;
80  import org.slf4j.LoggerFactory;
81  
82  
83  /**
84   * Provider implementation for Apache JackRabbit JCR repository.
85   *
86   * @version $Id$
87   */
88  public class ProviderImpl implements Provider {
89  
90      /**
91       * Magnolia property (entry in magnolia.properties) with the cluster id, which will be passed to jackrabbit.
92       */
93      private static final String MAGNOLIA_CLUSTERID_PROPERTY = "magnolia.clusterid";
94  
95      /**
96       * Jackrabbit system property for cluster node id.
97       */
98      private static final String JACKRABBIT_CLUSTER_ID_PROPERTY = "org.apache.jackrabbit.core.cluster.node_id";
99  
100     private static final Logger log = LoggerFactory.getLogger(ProviderImpl.class);
101 
102     private static final String CONFIG_FILENAME_KEY = "configFile";
103 
104     private static final String REPOSITORY_HOME_KEY = "repositoryHome";
105 
106     private static final String CONTEXT_FACTORY_CLASS_KEY = "contextFactoryClass";
107 
108     private static final String PROVIDER_URL_KEY = "providerURL";
109 
110     private static final String BIND_NAME_KEY = "bindName";
111 
112     private static final String MGNL_NODETYPES = "/mgnl-nodetypes/magnolia-nodetypes.xml";
113 
114     private static final String CUSTOM_NODETYPES = "customNodeTypes";
115 
116     private RepositoryDefinition repositoryMapping;
117 
118     private Repository repository;
119 
120     private String bindName;
121 
122     private Hashtable<String, Object> jndiEnv;
123 
124     private static final String REPO_HOME_PREFIX = "${repository.home}";
125 
126     private static final int REPO_HOME_SUFIX_LEN = REPO_HOME_PREFIX.length();
127 
128     private static final String sysRepositoryHome = System.getProperty("repository.home");
129 
130     private static final String sysRepositoryHomes = System.getProperty("repository.homes");
131 
132     /**
133      * Finds the physical path to the repository folder.
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         }
184         catch (IOException e1) {
185             // should never happen and it's not a problem at this point, just pass it to jackrabbit and see
186         }
187 
188         String clusterid = SystemProperty.getProperty(MAGNOLIA_CLUSTERID_PROPERTY);
189         if (StringUtils.isNotBlank(clusterid)) {
190             System.setProperty(JACKRABBIT_CLUSTER_ID_PROPERTY, clusterid);
191         }
192 
193         // get it back from system properties, if it has been set elsewhere
194         clusterid = System.getProperty(JACKRABBIT_CLUSTER_ID_PROPERTY);
195 
196         log.info("Loading repository at {} (config file: {}) - cluster id: \"{}\"", new Object[]{
197             repositoryHome,
198             configFile,
199             StringUtils.defaultString(clusterid, "<unset>")});
200 
201         bindName = (String) params.get(BIND_NAME_KEY);
202         jndiEnv = new Hashtable<String, Object>();
203         jndiEnv.put(Context.INITIAL_CONTEXT_FACTORY, params.get(CONTEXT_FACTORY_CLASS_KEY));
204         jndiEnv.put(Context.PROVIDER_URL, params.get(PROVIDER_URL_KEY));
205 
206         try {
207             InitialContext ctx = new InitialContext(jndiEnv);
208             // first try to find the existing object if any
209             try {
210                 this.repository = (Repository) ctx.lookup(bindName);
211             }
212             catch (NameNotFoundException ne) {
213                 log.debug("No JNDI bound Repository found with name {}, trying to initialize a new Repository", bindName);
214                 RegistryHelper.registerRepository(ctx, bindName, configFile, repositoryHome, true);
215                 this.repository = (Repository) ctx.lookup(bindName);
216             }
217             this.validateWorkspaces();
218         }
219         catch (NamingException e) {
220             log.error("Unable to initialize repository: " + e.getMessage(), e);
221             throw new RepositoryNotInitializedException(e);
222         }
223         catch (RepositoryException e) {
224             log.error("Unable to initialize repository: " + e.getMessage(), e);
225             throw new RepositoryNotInitializedException(e);
226         }
227         catch (TransformerFactoryConfigurationError e) {
228             log.error("Unable to initialize repository: " + e.getMessage(), e);
229             throw new RepositoryNotInitializedException(e);
230         }
231     }
232 
233     @Override
234     public void shutdownRepository() {
235         log.info("Shutting down repository bound to '{}'", bindName);
236 
237         try {
238             Context ctx = new InitialContext(jndiEnv);
239             RegistryHelper.unregisterRepository(ctx, bindName);
240         } catch (NamingException e) {
241             log.warn("Unable to shutdown repository " + bindName + ": " + e.getMessage(), e);
242         } catch (Throwable e) {
243             log.error("Failed to shutdown repository " + bindName + ": " + e.getMessage(), e);
244         }
245     }
246 
247     @Override
248     public Repository getUnderlyingRepository() throws RepositoryNotInitializedException {
249         if (this.repository == null) {
250             throw new RepositoryNotInitializedException("Null repository");
251         }
252         return this.repository;
253     }
254 
255     /**
256      * @see info.magnolia.repository.Provider#registerNamespace(java.lang.String, java.lang.String, javax.jcr.Workspace)
257      */
258     @Override
259     public void registerNamespace(String namespacePrefix, String uri, Workspace workspace) throws RepositoryException {
260         try {
261             workspace.getNamespaceRegistry().getURI(namespacePrefix);
262         }
263         catch (NamespaceException e) {
264             if (log.isDebugEnabled()) {
265                 log.debug(e.getMessage());
266             }
267             log.info("Registering prefix [{}] with URI {}", namespacePrefix, uri);
268             workspace.getNamespaceRegistry().registerNamespace(namespacePrefix, uri);
269         }
270     }
271 
272     /**
273      * @see info.magnolia.repository.Provider#unregisterNamespace(java.lang.String, javax.jcr.Workspace)
274      */
275     @Override
276     public void unregisterNamespace(String prefix, Workspace workspace) throws RepositoryException {
277         workspace.getNamespaceRegistry().unregisterNamespace(prefix);
278     }
279 
280     /**
281      * @see info.magnolia.repository.Provider#registerNodeTypes(String)
282      */
283     @Override
284     public void registerNodeTypes() throws RepositoryException {
285         registerNodeTypes(StringUtils.EMPTY);
286     }
287 
288     /**
289      * @see info.magnolia.repository.Provider#registerNodeTypes(java.lang.String)
290      */
291     @Override
292     public void registerNodeTypes(String configuration) throws RepositoryException {
293         if (StringUtils.isEmpty(configuration)) {
294             configuration = this.repositoryMapping.getParameters().get(CUSTOM_NODETYPES);
295         }
296 
297         InputStream xml = getNodeTypeDefinition(configuration);
298         this.registerNodeTypes(xml);
299     }
300 
301     /**
302      * @see info.magnolia.repository.Provider#registerNodeTypes(java.io.InputStream)
303      */
304     @Override
305     public void registerNodeTypes(InputStream xmlStream) throws RepositoryException {
306         SimpleCredentials credentials = new SimpleCredentials(
307             ContentRepository.REPOSITORY_USER,
308             ContentRepository.REPOSITORY_PSWD.toCharArray());
309         Session jcrSession = this.repository.login(credentials);
310 
311         try {
312 
313             Workspace workspace = jcrSession.getWorkspace();
314 
315             // should never happen
316             if (xmlStream == null) {
317                 throw new MissingNodetypesException();
318             }
319 
320             // Use Objects so that it works both with jackrabbit 1.x (NodeTypeDef) and jackrabbit 2
321             // (QNodeTypeDefinition)
322             Object[] types;
323 
324             try {
325                 types = (Object[]) NodeTypeReader.class.getMethod("read", new Class[]{InputStream.class}).invoke(
326                     null,
327                     new Object[]{xmlStream});
328             }
329             catch (Exception e) {
330                 throw new RepositoryException(e.getMessage(), e);
331             }
332             finally {
333                 IOUtils.closeQuietly(xmlStream);
334             }
335 
336             NodeTypeManager ntMgr = workspace.getNodeTypeManager();
337             NodeTypeRegistry ntReg;
338             try {
339                 ntReg = ((NodeTypeManagerImpl) ntMgr).getNodeTypeRegistry();
340             }
341             catch (ClassCastException e) {
342                 // this could happen if the repository provider does not have proper Shared API for the
343                 // application server like at the moment in Jackrabbit
344                 log.debug("Failed to get NodeTypeRegistry: ", e);
345                 return;
346             }
347 
348             for (int j = 0; j < types.length; j++) {
349                 Object def = types[j];
350 
351                 Name ntname;
352                 try {
353                     ntname = (Name) PropertyUtils.getProperty(def, "name");
354                 }
355                 catch (Exception e) {
356                     throw new RepositoryException(e.getMessage(), e);
357                 }
358 
359                 try {
360 
361                     // return value has changed in jackrabbit 2, we still have to use reflection here
362                     // ntReg.getNodeTypeDef(ntname);
363 
364                     Method method = ntReg.getClass().getMethod("getNodeTypeDef", Name.class);
365                     method.invoke(ntReg, ntname);
366                 }
367                 catch (IllegalArgumentException e)
368                 {
369                     throw new RepositoryException(e.getMessage(), e);
370                 }
371                 catch (IllegalAccessException e)
372                 {
373                     throw new RepositoryException(e.getMessage(), e);
374                 }
375                 catch (SecurityException e)
376                 {
377                     throw new RepositoryException(e.getMessage(), e);
378                 }
379                 catch (NoSuchMethodException e)
380                 {
381                     throw new RepositoryException(e.getMessage(), e);
382                 }
383                 catch (InvocationTargetException ite)
384                 {
385                     if (ite.getTargetException() instanceof NoSuchNodeTypeException)
386                     {
387                         log.info("Registering nodetype {} on repository {}", ntname, repositoryMapping.getName());
388 
389                         try
390                         {
391                             // reflection for jackrabbit 1+2 compatibility
392                             getMethod(NodeTypeRegistry.class, "registerNodeType").invoke(ntReg, new Object[]{def });
393                         }
394                         catch (Exception e)
395                         {
396                             throw new RepositoryException(e.getMessage(), e);
397                         }
398                     }
399                 }
400             }
401 
402         }
403         finally {
404             jcrSession.logout();
405         }
406     }
407 
408     private Method getMethod(Class theclass, String methodName) throws NoSuchMethodException {
409         Method[] declaredMethods = theclass.getDeclaredMethods();
410 
411         for (Method method : declaredMethods) {
412             if (method.getName().equals(methodName)) {
413                 return method;
414             }
415         }
416 
417         throw new NoSuchMethodException(theclass.getName() + "." + methodName + "()");
418     }
419 
420     /**
421      * @param configuration
422      * @return InputStream of node type definition file
423      */
424     private InputStream getNodeTypeDefinition(String configuration) {
425 
426         InputStream xml;
427 
428         if (StringUtils.isNotEmpty(configuration)) {
429 
430             // 1: try to load the configured file from the classpath
431             xml = getClass().getResourceAsStream(configuration);
432             if (xml != null) {
433                 log.info("Custom node types registered using {}", configuration);
434                 return xml;
435             }
436 
437             // 2: try to load it from the file system
438             File nodeTypeDefinition = new File(Path.getAbsoluteFileSystemPath(configuration));
439             if (nodeTypeDefinition.exists()) {
440                 try {
441                     return new FileInputStream(nodeTypeDefinition);
442                 }
443                 catch (FileNotFoundException e) {
444                     // should never happen
445                     log.error("File not found: {}", configuration);
446                 }
447             }
448 
449             // 3: defaults to standard nodetypes
450             log.error("Unable to find node type definition: {} for repository {}", configuration, this.repositoryMapping.getName());
451         }
452 
453         // initialize default magnolia nodetypes
454         xml = getClass().getResourceAsStream(MGNL_NODETYPES);
455 
456         return xml;
457     }
458 
459     /**
460      * WORKAROUND for tomcat 5.0/jdk 1.5 problem tomcat\common\endorsed contains an xml-apis.jar needed by tomcat and
461      * loaded before all xmsl stuff present in the jdk (1.4 naming problem). In the xml-apis.jar file the
462      * TransformerFactoryImpl is set to "org.apache.xalan.processor.TransformerFactoryImpl" instead of
463      * "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl". solution: remove the file xml-apis.jar
464      * from the directory OR manually change the javax.xml.transform.TransformerFactory system property
465      */
466     protected void checkXmlSettings() {
467         if (SystemUtils.isJavaVersionAtLeast(1.5f)
468             && "org.apache.xalan.processor.TransformerFactoryImpl".equals(System
469                 .getProperty("javax.xml.transform.TransformerFactory"))) {
470 
471             String transformerClass = "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl";
472 
473             try {
474                 Class.forName(transformerClass);
475 
476                 System.setProperty("javax.xml.transform.TransformerFactory", transformerClass);
477 
478                 log.info("Java 1.5 detected, setting system property \"javax.xml.transform.TransformerFactory\" to \"{}\"", transformerClass);
479             }
480             catch (Throwable e) {
481                 // not in the classpath. We can't assume which one to use, so just go on
482             }
483         }
484     }
485 
486     /**
487      * Checks if all workspaces are present according to the repository mapping, creates any missing workspace.
488      */
489     private void validateWorkspaces() throws RepositoryException {
490         Iterator<String> configuredNames = repositoryMapping.getWorkspaces().iterator();
491         while (configuredNames.hasNext()) {
492             registerWorkspace(configuredNames.next());
493         }
494     }
495 
496     /**
497      * @see info.magnolia.repository.Provider#registerWorkspace(java.lang.String)
498      */
499     @Override
500     public boolean registerWorkspace(String workspaceName) throws RepositoryException {
501         // check if workspace already exists
502         SimpleCredentials credentials = new SimpleCredentials(
503             ContentRepository.REPOSITORY_USER,
504             ContentRepository.REPOSITORY_PSWD.toCharArray());
505         Session jcrSession = this.repository.login(credentials);
506 
507         try {
508             WorkspaceImpl defaultWorkspace = (WorkspaceImpl) jcrSession.getWorkspace();
509             String[] workspaceNames = defaultWorkspace.getAccessibleWorkspaceNames();
510 
511             boolean alreadyExists = ArrayUtils.contains(workspaceNames, workspaceName);
512             if (!alreadyExists) {
513                 defaultWorkspace.createWorkspace(workspaceName);
514             }
515             jcrSession.logout();
516 
517             return !alreadyExists;
518         } catch (ClassCastException e) {
519             // this could happen if the repository provider does not have proper Shared API for the
520             // application server like at the moment in Jackrabbit
521             log.debug("Unable to register workspace, will continue", e);
522         } catch (Throwable t) {
523             log.error("Unable to register workspace, will continue", t);
524         }
525         return false;
526     }
527 
528     @Override
529     public Session getSystemSession(String workspaceName) throws RepositoryException {
530 
531         // 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
532 
533         String user = SystemProperty.getProperty("magnolia.connection.jcr.admin.userId", SystemProperty.getProperty("magnolia.connection.jcr.userId", "admin"));
534         String pwd = SystemProperty.getProperty("magnolia.connection.jcr.admin.password", SystemProperty.getProperty("magnolia.connection.jcr.password", "admin"));
535         return this.repository.login(new SimpleCredentials(user, pwd.toCharArray()), workspaceName);
536     }
537 
538 }