View Javadoc
1   /**
2    * This file Copyright (c) 2003-2018 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.core.FileSystemHelper;
37  import info.magnolia.cms.core.SystemProperty;
38  import info.magnolia.init.MagnoliaConfigurationProperties;
39  import info.magnolia.objectfactory.Components;
40  import info.magnolia.repository.Provider;
41  import info.magnolia.repository.RepositoryNotInitializedException;
42  import info.magnolia.repository.definition.RepositoryDefinition;
43  
44  import java.io.ByteArrayInputStream;
45  import java.io.ByteArrayOutputStream;
46  import java.io.File;
47  import java.io.FileInputStream;
48  import java.io.FileNotFoundException;
49  import java.io.IOException;
50  import java.io.InputStream;
51  import java.io.InputStreamReader;
52  import java.lang.reflect.InvocationTargetException;
53  import java.lang.reflect.Method;
54  import java.util.Hashtable;
55  import java.util.Iterator;
56  import java.util.Map;
57  
58  import javax.inject.Inject;
59  import javax.jcr.NamespaceException;
60  import javax.jcr.Repository;
61  import javax.jcr.RepositoryException;
62  import javax.jcr.Session;
63  import javax.jcr.SimpleCredentials;
64  import javax.jcr.Workspace;
65  import javax.jcr.nodetype.NoSuchNodeTypeException;
66  import javax.jcr.nodetype.NodeTypeManager;
67  import javax.naming.Context;
68  import javax.naming.InitialContext;
69  import javax.naming.NameNotFoundException;
70  import javax.naming.NamingException;
71  import javax.xml.parsers.ParserConfigurationException;
72  import javax.xml.transform.TransformerException;
73  import javax.xml.transform.TransformerFactoryConfigurationError;
74  
75  import org.apache.commons.beanutils.PropertyUtils;
76  import org.apache.commons.io.IOUtils;
77  import org.apache.commons.lang3.ArrayUtils;
78  import org.apache.commons.lang3.JavaVersion;
79  import org.apache.commons.lang3.StringUtils;
80  import org.apache.commons.lang3.SystemUtils;
81  import org.apache.jackrabbit.api.JackrabbitWorkspace;
82  import org.apache.jackrabbit.commons.cnd.CndImporter;
83  import org.apache.jackrabbit.commons.cnd.ParseException;
84  import org.apache.jackrabbit.core.jndi.RegistryHelper;
85  import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl;
86  import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
87  import org.apache.jackrabbit.core.nodetype.xml.NodeTypeReader;
88  import org.apache.jackrabbit.spi.Name;
89  import org.slf4j.Logger;
90  import org.slf4j.LoggerFactory;
91  import org.xml.sax.InputSource;
92  import org.xml.sax.SAXException;
93  
94  
95  /**
96   * Provider implementation for Apache JackRabbit JCR repository.
97   */
98  public class ProviderImpl implements Provider {
99      /**
100      * Magnolia property (entry in magnolia.properties) with the cluster id, which will be passed to jackrabbit.
101      */
102     private static final String MAGNOLIA_CLUSTERID_PROPERTY = "magnolia.clusterid";
103 
104 
105     /**
106      * Jackrabbit system property for cluster node id.
107      */
108     private static final String JACKRABBIT_CLUSTER_ID_PROPERTY = "org.apache.jackrabbit.core.cluster.node_id";
109 
110     private static final Logger log = LoggerFactory.getLogger(ProviderImpl.class);
111 
112     private static final String CONFIG_FILENAME_KEY = "configFile";
113 
114     private static final String REPOSITORY_HOME_KEY = "repositoryHome";
115 
116     private static final String CONTEXT_FACTORY_CLASS_KEY = "contextFactoryClass";
117 
118     private static final String PROVIDER_URL_KEY = "providerURL";
119 
120     private static final String BIND_NAME_KEY = "bindName";
121 
122     private static final String MGNL_NODETYPES = "/mgnl-nodetypes/magnolia-nodetypes.cnd";
123 
124     private static final String CUSTOM_NODETYPES = "customNodeTypes";
125 
126 
127     private RepositoryDefinition repositoryMapping;
128 
129     private Repository repository;
130 
131     private String bindName;
132 
133     private Hashtable<String, Object> jndiEnv;
134 
135     private static final String REPO_HOME_PREFIX = "${repository.home}";
136 
137     private static final int REPO_HOME_SUFIX_LEN = REPO_HOME_PREFIX.length();
138 
139     private static final String sysRepositoryHome = System.getProperty("repository.home");
140 
141     private static final String sysRepositoryHomes = System.getProperty("repository.homes");
142 
143     private final FileSystemHelper fileSystemHelper;
144     private final MagnoliaConfigurationProperties configurationProperties;
145 
146     @Inject
147     public ProviderImpl(FileSystemHelper fileSystemHelper, MagnoliaConfigurationProperties configurationProperties) {
148         this.fileSystemHelper = fileSystemHelper;
149         this.configurationProperties = configurationProperties;
150     }
151 
152     /**
153      * @deprecated since 6.1 - use {@link #ProviderImpl(FileSystemHelper, MagnoliaConfigurationProperties) instead.
154      */
155     @Deprecated
156     public ProviderImpl(FileSystemHelper fileSystemHelper) {
157         this(fileSystemHelper, Components.getComponent(MagnoliaConfigurationProperties.class));
158     }
159 
160     /**
161      * @deprecated since 6.1 - use {@link #ProviderImpl(FileSystemHelper, MagnoliaConfigurationProperties) instead.
162      */
163     @Deprecated
164     public ProviderImpl() {
165         this(Components.getComponent(FileSystemHelper.class), Components.getComponent(MagnoliaConfigurationProperties.class));
166     }
167 
168     /**
169      * Finds the physical path to the repository folder.
170      *
171      * @param repositoryHome the property set in the repository.xml file
172      * @return the full path resolved to the repository dir
173      */
174     private String getRepositoryHome(final String repositoryHome) {
175         boolean relocate = false;
176         String tmp = repositoryHome;
177         if (repositoryHome.startsWith(REPOSITORY_HOME_KEY)) {
178             tmp = repositoryHome.substring(REPO_HOME_SUFIX_LEN);
179             relocate = true;
180         }
181         /*
182          * Resolve if the path started with the suffix
183          */
184         if (sysRepositoryHome != null && relocate) {
185             return sysRepositoryHome + File.separator + tmp;
186         }
187 
188         /*
189          * This is apply to all repositories if the java property is set
190          */
191         if (sysRepositoryHomes != null) {
192             return sysRepositoryHomes + File.separator + tmp;
193         }
194 
195         /*
196          * Return the same value as before if neither of the above applied
197          */
198         return fileSystemHelper.getAbsoluteFileSystemPath(tmp);
199     }
200 
201     /**
202      * @see info.magnolia.repository.Provider#init(info.magnolia.repository.definition.RepositoryDefinition)
203      */
204     @Override
205     public void init(RepositoryDefinition repositoryMapping) throws RepositoryNotInitializedException {
206         checkXmlSettings();
207 
208         this.repositoryMapping = repositoryMapping;
209         /* connect to repository */
210         Map params = this.repositoryMapping.getParameters();
211         String configFile = (String) params.get(CONFIG_FILENAME_KEY);
212         configFile = fileSystemHelper.getAbsoluteFileSystemPath(configFile);
213         String repositoryHome = (String) params.get(REPOSITORY_HOME_KEY);
214         repositoryHome = getRepositoryHome(repositoryHome);
215 
216         // cleanup the path, to remove eventual ../.. and make it absolute
217         try {
218             File repoHomeFile = new File(repositoryHome);
219             repositoryHome = repoHomeFile.getCanonicalPath();
220         } catch (IOException e1) {
221             // should never happen and it's not a problem at this point, just pass it to jackrabbit and see
222         }
223 
224         String clusterid = SystemProperty.getProperty(MAGNOLIA_CLUSTERID_PROPERTY);
225         if (StringUtils.isNotBlank(clusterid)) {
226             System.setProperty(JACKRABBIT_CLUSTER_ID_PROPERTY, clusterid);
227         }
228 
229         // get it back from system properties, if it has been set elsewhere
230         clusterid = System.getProperty(JACKRABBIT_CLUSTER_ID_PROPERTY);
231 
232         log.info("Loading repository at {} (config file: {}) - cluster id: \"{}\"", repositoryHome, configFile, StringUtils.defaultString(clusterid, "<unset>"));
233 
234         bindName = (String) params.get(BIND_NAME_KEY);
235         jndiEnv = new Hashtable<String, Object>();
236         jndiEnv.put(Context.INITIAL_CONTEXT_FACTORY, params.get(CONTEXT_FACTORY_CLASS_KEY));
237         jndiEnv.put(Context.PROVIDER_URL, params.get(PROVIDER_URL_KEY));
238 
239         try {
240             InitialContext ctx = new InitialContext(jndiEnv);
241             // first try to find the existing object if any
242             try {
243                 this.repository = (Repository) ctx.lookup(bindName);
244             } catch (NameNotFoundException ne) {
245                 log.debug("No JNDI bound Repository found with name {}, trying to initialize a new Repository", bindName);
246                 RegistryHelper.registerRepository(ctx, bindName, configFile, repositoryHome, true);
247                 this.repository = (Repository) ctx.lookup(bindName);
248             }
249             this.validateWorkspaces();
250         } catch (NamingException e) {
251             log.error("Unable to initialize repository: {}", e.getMessage(), e);
252             throw new RepositoryNotInitializedException(e);
253         } catch (RepositoryException e) {
254             log.error("Unable to initialize repository: {}", e.getMessage(), e);
255             throw new RepositoryNotInitializedException(e);
256         } catch (TransformerFactoryConfigurationError e) {
257             log.error("Unable to initialize repository: {}", e.getMessage(), e);
258             throw new RepositoryNotInitializedException(e);
259         }
260     }
261 
262     @Override
263     public void shutdownRepository() {
264         log.info("Shutting down repository bound to '{}'", bindName);
265 
266         try {
267             Context ctx = new InitialContext(jndiEnv);
268             RegistryHelper.unregisterRepository(ctx, bindName);
269         } catch (NamingException e) {
270             log.warn("Unable to shutdown repository {}: {}", bindName, e.getMessage(), e);
271         } catch (Throwable e) {
272             log.error("Failed to shutdown repository {}: {}", bindName, e.getMessage(), e);
273         }
274     }
275 
276     @Override
277     public Repository getUnderlyingRepository() throws RepositoryNotInitializedException {
278         if (this.repository == null) {
279             throw new RepositoryNotInitializedException("Null repository");
280         }
281         return this.repository;
282     }
283 
284     /**
285      * @see info.magnolia.repository.Provider#registerNamespace(java.lang.String, java.lang.String, javax.jcr.Workspace)
286      */
287     @Override
288     public void registerNamespace(String namespacePrefix, String uri, Workspace workspace) throws RepositoryException {
289         try {
290             workspace.getNamespaceRegistry().getURI(namespacePrefix);
291         } catch (NamespaceException e) {
292             log.debug(e.getMessage());
293 
294             log.info("Registering prefix [{}] with URI {}", namespacePrefix, uri);
295             workspace.getNamespaceRegistry().registerNamespace(namespacePrefix, uri);
296         }
297     }
298 
299     /**
300      * @see info.magnolia.repository.Provider#unregisterNamespace(java.lang.String, javax.jcr.Workspace)
301      */
302     @Override
303     public void unregisterNamespace(String prefix, Workspace workspace) throws RepositoryException {
304         workspace.getNamespaceRegistry().unregisterNamespace(prefix);
305     }
306 
307     /**
308      * @see info.magnolia.repository.Provider#registerNodeTypes(String)
309      */
310     @Override
311     public void registerNodeTypes() throws RepositoryException {
312         registerNodeTypes(StringUtils.EMPTY);
313     }
314 
315     /**
316      * @see info.magnolia.repository.Provider#registerNodeTypes(java.lang.String)
317      */
318     @Override
319     public void registerNodeTypes(String path) throws RepositoryException {
320         if (StringUtils.isEmpty(path)) {
321             path = this.repositoryMapping.getParameters().get(CUSTOM_NODETYPES);
322         }
323 
324         InputStream configuration = getNodeTypeDefinition(path);
325         if (StringUtils.isEmpty(path) || path.endsWith(".cnd")) {
326             SimpleCredentials credentials = new SimpleCredentials(
327                     configurationProperties.getProperty(MagnoliaConfigurationProperties.MAGNOLIA_JACKRABBIT_USER_PROPERTY),
328                     configurationProperties.getProperty(MagnoliaConfigurationProperties.MAGNOLIA_JACKRABBIT_PASSWORD_PROPERTY).toCharArray());
329             Session jcrSession = this.repository.login(credentials);
330 
331             try {
332                 CndImporter.registerNodeTypes(new InputStreamReader(configuration), jcrSession);
333                 jcrSession.logout();
334             } catch (ParseException e) {
335                 log.warn("Failed to register nodetypes from {}: ", StringUtils.defaultIfEmpty(path, MGNL_NODETYPES));
336             } catch (IOException e) {
337                 log.warn("Failed to register nodetypes from {}: ", StringUtils.defaultIfEmpty(path, MGNL_NODETYPES));
338             }
339         } else {
340             this.registerNodeTypes(configuration);
341         }
342     }
343 
344     /**
345      * @see info.magnolia.repository.Provider#registerNodeTypes(java.io.InputStream)
346      */
347     @Override
348     public void registerNodeTypes(InputStream xmlStream) throws RepositoryException {
349         SimpleCredentials credentials = new SimpleCredentials(
350                 configurationProperties.getProperty(MagnoliaConfigurationProperties.MAGNOLIA_JACKRABBIT_USER_PROPERTY),
351                 configurationProperties.getProperty(MagnoliaConfigurationProperties.MAGNOLIA_JACKRABBIT_PASSWORD_PROPERTY).toCharArray());
352         Session jcrSession = this.repository.login(credentials);
353 
354         try {
355             Workspace workspace = jcrSession.getWorkspace();
356 
357             // should never happen
358             if (xmlStream == null) {
359                 throw new MissingNodetypesException();
360             }
361 
362             // Use Objects so that it works both with jackrabbit 1.x (NodeTypeDef) and jackrabbit 2
363             // (QNodeTypeDefinition)
364             Object[] types;
365 
366             try {
367                 types = (Object[]) NodeTypeReader.class.getMethod("read", new Class[]{InputStream.class}).invoke(null, xmlStream);
368             } catch (Exception e) {
369                 throw new RepositoryException(e.getMessage(), e);
370             } finally {
371                 IOUtils.closeQuietly(xmlStream);
372             }
373 
374             NodeTypeManager ntMgr = workspace.getNodeTypeManager();
375             NodeTypeRegistry ntReg;
376             try {
377                 ntReg = ((NodeTypeManagerImpl) ntMgr).getNodeTypeRegistry();
378             } catch (ClassCastException e) {
379                 // this could happen if the repository provider does not have proper Shared API for the
380                 // application server like at the moment in Jackrabbit
381                 log.debug("Failed to get NodeTypeRegistry: ", e);
382                 return;
383             }
384 
385             for (int j = 0; j < types.length; j++) {
386                 Object def = types[j];
387 
388                 Name ntname;
389                 try {
390                     ntname = (Name) PropertyUtils.getProperty(def, "name");
391                 } catch (Exception e) {
392                     throw new RepositoryException(e.getMessage(), e);
393                 }
394 
395                 try {
396 
397                     // return value has changed in jackrabbit 2, we still have to use reflection here
398                     // ntReg.getNodeTypeDef(ntname);
399 
400                     Method method = ntReg.getClass().getMethod("getNodeTypeDef", Name.class);
401                     method.invoke(ntReg, ntname);
402                 } catch (IllegalArgumentException e) {
403                     throw new RepositoryException(e.getMessage(), e);
404                 } catch (IllegalAccessException e) {
405                     throw new RepositoryException(e.getMessage(), e);
406                 } catch (SecurityException e) {
407                     throw new RepositoryException(e.getMessage(), e);
408                 } catch (NoSuchMethodException e) {
409                     throw new RepositoryException(e.getMessage(), e);
410                 } catch (InvocationTargetException ite) {
411                     if (ite.getTargetException() instanceof NoSuchNodeTypeException) {
412                         log.info("Registering nodetype {} on repository {}", ntname, repositoryMapping.getName());
413 
414                         try {
415                             // reflection for jackrabbit 1+2 compatibility
416                             getMethod(NodeTypeRegistry.class, "registerNodeType").invoke(ntReg, def);
417                         } catch (Exception e) {
418                             throw new RepositoryException(e.getMessage(), e);
419                         }
420                     }
421                 }
422             }
423         } finally {
424             jcrSession.logout();
425         }
426     }
427 
428     private Method getMethod(Class theclass, String methodName) throws NoSuchMethodException {
429         Method[] declaredMethods = theclass.getDeclaredMethods();
430 
431         for (Method method : declaredMethods) {
432             if (method.getName().equals(methodName)) {
433                 return method;
434             }
435         }
436 
437         throw new NoSuchMethodException(theclass.getName() + "." + methodName + "()");
438     }
439 
440     /**
441      * @return InputStream of node type definition file
442      */
443     private InputStream getNodeTypeDefinition(String configuration) {
444 
445         InputStream xml;
446 
447         if (StringUtils.isNotEmpty(configuration)) {
448 
449             // 1: try to load the configured file from the classpath
450             xml = getClass().getResourceAsStream(configuration);
451             if (xml != null) {
452                 log.info("Custom node types registered using {}", configuration);
453                 return xml;
454             }
455 
456             // 2: try to load it from the file system
457             File nodeTypeDefinition = new File(fileSystemHelper.getAbsoluteFileSystemPath(configuration));
458             if (nodeTypeDefinition.exists()) {
459                 try {
460                     return new FileInputStream(nodeTypeDefinition);
461                 } catch (FileNotFoundException e) {
462                     // should never happen
463                     log.error("File not found: {}", configuration);
464                 }
465             }
466 
467             // 3: defaults to standard nodetypes
468             log.error("Unable to find node type definition: {} for repository {}", configuration, this.repositoryMapping.getName());
469         }
470 
471         // initialize default magnolia nodetypes
472         xml = getClass().getResourceAsStream(MGNL_NODETYPES);
473 
474         return xml;
475     }
476 
477     /**
478      * WORKAROUND for tomcat 5.0/jdk 1.5 problem tomcat\common\endorsed contains an xml-apis.jar needed by tomcat and
479      * loaded before all xmsl stuff present in the jdk (1.4 naming problem). In the xml-apis.jar file the
480      * TransformerFactoryImpl is set to "org.apache.xalan.processor.TransformerFactoryImpl" instead of
481      * "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl". solution: remove the file xml-apis.jar
482      * from the directory OR manually change the javax.xml.transform.TransformerFactory system property
483      */
484     protected void checkXmlSettings() {
485         if (SystemUtils.isJavaVersionAtLeast(JavaVersion.JAVA_1_5)
486                 && "org.apache.xalan.processor.TransformerFactoryImpl".equals(System
487                 .getProperty("javax.xml.transform.TransformerFactory"))) {
488 
489             String transformerClass = "com.sun.org.apache.xalan.internal.xsltc.trax.TransformerFactoryImpl";
490 
491             try {
492                 Class.forName(transformerClass);
493 
494                 System.setProperty("javax.xml.transform.TransformerFactory", transformerClass);
495 
496                 log.info("Java 1.5 detected, setting system property \"javax.xml.transform.TransformerFactory\" to \"{}\"", transformerClass);
497             } catch (Throwable e) {
498                 // not in the classpath. We can't assume which one to use, so just go on
499             }
500         }
501     }
502 
503     /**
504      * Checks if all workspaces are present according to the repository mapping, creates any missing workspace.
505      */
506     private void validateWorkspaces() throws RepositoryException {
507         Iterator<String> configuredNames = repositoryMapping.getWorkspaces().iterator();
508         while (configuredNames.hasNext()) {
509             registerWorkspace(configuredNames.next());
510         }
511     }
512 
513     /**
514      * @see info.magnolia.repository.Provider#registerWorkspace(java.lang.String)
515      */
516     @Override
517     public boolean registerWorkspace(String workspaceName) throws RepositoryException {
518         // check if workspace already exists
519         SimpleCredentials credentials = new SimpleCredentials(
520                 configurationProperties.getProperty(MagnoliaConfigurationProperties.MAGNOLIA_JACKRABBIT_USER_PROPERTY),
521                 configurationProperties.getProperty(MagnoliaConfigurationProperties.MAGNOLIA_JACKRABBIT_PASSWORD_PROPERTY).toCharArray());
522         Session jcrSession = this.repository.login(credentials);
523 
524         try {
525             try {
526                 JackrabbitWorkspace defaultWorkspace = (JackrabbitWorkspace) jcrSession.getWorkspace();
527                 String[] workspaceNames = defaultWorkspace.getAccessibleWorkspaceNames();
528 
529                 boolean alreadyExists = ArrayUtils.contains(workspaceNames, workspaceName);
530                 if (!alreadyExists) {
531                     createWorkspace(workspaceName, defaultWorkspace);
532                 }
533                 return !alreadyExists;
534             } catch (ClassCastException e) {
535                 // this could happen if the repository provider does not have proper Shared API for the
536                 // application server like at the moment in Jackrabbit
537                 log.debug("Unable to register workspace, will continue", e);
538             } catch (Throwable t) {
539                 log.error("Unable to register workspace, will continue", t);
540             }
541         } finally {
542             jcrSession.logout();
543         }
544         return false;
545     }
546 
547     private void createWorkspace(String workspaceName, JackrabbitWorkspace defaultWorkspace)
548             throws RepositoryException, ParserConfigurationException, IOException, SAXException, TransformerException {
549         InputStream workspaceStubXml = getClass().getResourceAsStream("/info/magnolia/jackrabbit/workspace_configuration_" + workspaceName + ".xml");
550         if (workspaceStubXml == null) {
551             defaultWorkspace.createWorkspace(workspaceName);
552         } else {
553             String configFile = repositoryMapping.getParameters().get(CONFIG_FILENAME_KEY);
554             InputStream repositoryXml = new FileInputStream(fileSystemHelper.getAbsoluteFileSystemPath(configFile));
555             ByteArrayOutputStream workspaceXml = new ByteArrayOutputStream();
556             try {
557                 WorkspaceConfigurationMerger.merge(repositoryXml, workspaceStubXml, workspaceXml);
558                 defaultWorkspace.createWorkspace(workspaceName, new InputSource(new ByteArrayInputStream(workspaceXml.toByteArray())));
559             } finally {
560                 repositoryXml.close();
561                 workspaceStubXml.close();
562                 workspaceXml.close();
563             }
564         }
565     }
566 
567     @Override
568     public Session getSystemSession(String workspaceName) throws RepositoryException {
569         String user = configurationProperties.getProperty(MagnoliaConfigurationProperties.MAGNOLIA_JACKRABBIT_USER_PROPERTY);
570         String pwd = configurationProperties.getProperty(MagnoliaConfigurationProperties.MAGNOLIA_JACKRABBIT_PASSWORD_PROPERTY);
571         return this.repository.login(new SimpleCredentials(user, pwd.toCharArray()), workspaceName);
572     }
573 
574 }