Tuesday, July 20, 2010

How to access Jackrabbit content repository via JNDI?

In this post I described how an Jackrabbit content repository can be set up. I will show now how it can be accessed via JNDI. I dont' want to write an JNDI lookup although it's not difficult. I would like to use Google Guice, a dependency injection framework. At first we need a Guice configuration module.
public class DefaultConfigurationGuiceModule extends AbstractModule
{
    protected void configure()
    {
        bind(String.class).annotatedWith(Names.named("repository name")).toInstance("jcr/repository");

        // bind naming context to the default InitialContext
        bind(Context.class).to(InitialContext.class);

        // bind to the repository from JNDI
        bind(Repository.class).toProvider(JndiIntegration.fromJndi(Repository.class, "jcr/repository"));

        // bind to the factory class for the creation of repository accessor
        // see http://code.google.com/docreader/#p=google-guice&s=google-guice&t=AssistedInject
        bind(RepositoryAccessorFactory.class).toProvider(FactoryProvider.newFactory(RepositoryAccessorFactory.class,
                                             JackrabbitRepositoryAccessor.class)).in(Singleton.class);

    }
}
Guice has a helpful class JndiIntegration to create a provider which looks up objects in JNDI using the given name. Furthermore I use Guice's AssistedInject and define a factory interface to create an instance for repository access (JackrabbitRepositoryAccessor). The real factory will be created by AssistedInject.
public interface RepositoryAccessorFactory
{
    /**
     * Greates an instance of {@link RepositoryAccessor}. Sets an input stream of XML file which describes custom node
     * types and appropriated custom namespace mapping. Custom node types and namespace will be registered one-time if
     * the JCR session is requested and they were not registered yet.
     *
     * @param  nodeTypeConfigs configurations of custom node types to be registered
     * @return RepositoryAccessor the instance of {@link RepositoryAccessor}
     */
    RepositoryAccessor create(@Assisted final NodeTypeConfig[] nodeTypeConfigs);
}
The class NodeTypeConfig is used for the registration of custom node types. Node types are described here. More about custom node types in XML notation see in my previous post.
/**
 * Configuration infos about a node type to be registered.
 */
public class NodeTypeConfig
{
    /** input stream of XML file which describes node types */
    private InputStream inputStream;

    /** namespace prefix of the node type */
    private String namespacePrefix;

    /** namespace uri of the node type */
    private String namespaceUri;

    /**
     * Creates a new NodeTypeConfig object.
     *
     * @param inputStream     input stream of XML file which describes node types
     * @param namespacePrefix namespace prefix of the node type
     * @param namespaceUri    namespace uri of the node type
     */
    public NodeTypeConfig(final InputStream inputStream, final String namespacePrefix, final String namespaceUri)
    {
        this.inputStream = inputStream;
        this.namespacePrefix = namespacePrefix;
        this.namespaceUri = namespaceUri;
    }

    setter / getter ...

    /**
    * Loads node type configuration from XML file in classpath.
    *
    * @param  fileName        file name
    * @param  namespacePrefix namespace prefix of the node type
    * @param  namespaceUri    namespace uri of the node type
    * @return NodeTypeConfig configuration
    */
    public static NodeTypeConfig getNodeTypeConfig(final String fileName, final String namespacePrefix, final String namespaceUri)
    {
        InputStream inputStream = getInputStreamConfig(fileName);
        return new NodeTypeConfig(inputStream, namespacePrefix, namespaceUri);
    }

    /**
     * Gets input stream from XML file in classpath.
     *
     * @param  fileName file name
     * @return NodeTypeConfig configuration
     */
    public static InputStream getInputStreamConfig(final String fileName)
    {
        Validate.notNull(fileName, "XML file with node type configuration is null");

        ClassLoader classloader = Thread.currentThread().getContextClassLoader();
        if (classloader == null) {
            classloader = NodeTypeConfig.class.getClassLoader();
        }

        return classloader.getResourceAsStream(fileName);
    }
}
The most important class is JackrabbitRepositoryAccessor. This is an entry point into the content repository. This class implements an interface RepositoryAccessor. This interface looks as follows
public interface RepositoryAccessor
{
 /**
  * Gets the content repository. If no repository has been yet created it will be created.
  *
  * @see    #startRepository()
  * @return Repository repository {@link Repository}
  * @throws RepositoryException if the repository could be not acquired
  */
 Repository getRepository() throws RepositoryException;

 /**
  * Starts and initializes the content repository by the configured repository name via JNDI.
  *
  * @throws RepositoryException if the repository could be not acquired or an error occured
  */
 void startRepository() throws RepositoryException;

 /**
  * Retrieves the current JCR Session local to the thread which it is tied to one workspase. If no JCR Session is
  * open, opens a new JCR Session for the running thread.
  *
  * @param  workspaceName name of the workspace (<code>null</code> is not allowed)
  * @param  viewerId      viewer id from {@link SecurityToken}
  * @return Session JCR session {@link Session}
  * @throws LoginException            if the login fails
  * @throws NoSuchWorkspaceException  if a specific workspace is not found
  * @throws AccessDeniedException     if the session associated with the workspace object does not have sufficient
  *                                   permissions to register core / custom namespaces, to create a new workspace or
  *                                   some access-related methods failed
  * @throws NamespaceException        if an illegal attempt is made to register a mapping
  * @throws RegisterNodeTypeException if registration of core / custom node type(s) failed
  * @throws RepositoryException       if the repository could be not acquired or an error occured
  */
 Session getSession(final String workspaceName, final String viewerId)
     throws LoginException, NoSuchWorkspaceException, AccessDeniedException, NamespaceException,
            RegisterNodeTypeException, RepositoryException;

 /**
  * Closes all JCR Sessions local to the thread.
  */
 void releaseSession();

 /**
  * Releases the content repository
  */
 void releaseRepository();
}
And the implementation (a little bit big code) looks as follows
public class JackrabbitRepositoryAccessor implements RepositoryAccessor
{
 private static final Logger LOG = LoggerFactory.getLogger(RepositoryAccessor.class);

 private static final ThreadLocal<Map<String, Session>> THREAD_SESSION = new ThreadLocal<Map<String, Session>>();

 /** repository instance */
 private Repository repository;

 /** defauilt workspace */
 private Workspace defaultWorkspace;

 /** repository name (not mandatory) */
 @Inject(optional = true)
 @Named("repository name")
 private String repositoryName;

 /** flag whether the core namespace mapping and node types were already registered */
 private boolean isRegistered = false;

 /** flag whether the custom namespace mapping and node types were already registered */
 private boolean isCustomRegistered = false;

 /** input stream of XML file which describes custom node types (not mandatory) */
 private NodeTypeConfig[] customNodeTypeConfigs;

 /** provider for repository */
 private Provider<Repository> repositoryProvider;

 /**
  * Creates a new <code>JackrabbitRepositoryAccessor</code> object and sets repository providers.
  * Note: Custom node types and namespace will be registered one-time if the JCR session is
  * requested and they were not registered yet.
  *
  * @param repositoryProvider                  repository provider to get an access to the configured repository
  *                                            {@link Repository}
  * @param customNodeTypeConfigs               custom node types configurations (if <code>null</code> no custom node
  *                                            types will be registered)
  */
 @Inject
 public JackrabbitRepositoryAccessor(final Provider<Repository> repositoryProvider,
                                     @Assisted
                                     @Nullable
                                     final NodeTypeConfig[] customNodeTypeConfigs)
 {
  // set repository provider
  this.repositoryProvider = repositoryProvider;

  this.customNodeTypeConfigs = customNodeTypeConfigs;
 }

 //~ Methods ----------------------------------------------------------------

 /**
  * Gets the default workspace. If no default workspace has been yet created it will be created.
  *
  * @see    #startRepository()
  * @return Workspace default workspace {@link Workspace}
  * @throws RepositoryException if the repository or workspace could be not acquired
  */
 protected Workspace getDefaultWorkspace() throws RepositoryException
 {
  if (defaultWorkspace == null) {
   synchronized (JackrabbitRepositoryAccessor.class) {
    Repository repository = getRepository();
    if (defaultWorkspace == null) {
     defaultWorkspace = repository.login().getWorkspace();
     if (LOG.isDebugEnabled()) {
      LOG.debug("==> Default workspace '"
                + (defaultWorkspace != null ? defaultWorkspace.getName() : "null")
                + "' acquired.");
     }
    }
   }
  }

  return defaultWorkspace;
 }

 /**
  * Registers a node type.
  *
  * @param  jcrSession  current JCR session
  * @param  inputStream input stream of XML file which describes node types
  * @throws RegisterNodeTypeException if registration of core / custom node type failed
  * @throws RepositoryException       if an error occured
  */
 @SuppressWarnings("unchecked")
 protected void registerNodeType(final Session jcrSession, final InputStream inputStream)
     throws RegisterNodeTypeException, RepositoryException
 {
  try {
   NodeTypeManagerImpl ntManager = (NodeTypeManagerImpl) jcrSession.getWorkspace().getNodeTypeManager();
   NodeTypeRegistry ntRegistry = ntManager.getNodeTypeRegistry();
   NodeTypeDefStore ntDefStore = new NodeTypeDefStore();

   ntDefStore.load(inputStream);

   Collection<NodeTypeDef> ntDefs = ntDefStore.all();
   Iterator<NodeTypeDef> iter = ntDefs.iterator();
   while (iter.hasNext()) {
    NodeTypeDef ntDef = iter.next();
    if (!ntRegistry.isRegistered(ntDef.getName())) {
     ntRegistry.registerNodeType(ntDef);
    }
   }
  } catch (IOException e) {
   throw new RegisterNodeTypeException(e);
  } catch (InvalidNodeTypeDefException e) {
   throw new RegisterNodeTypeException(e);
  } finally {
   IOUtils.closeQuietly(inputStream);
  }
 }
 
 /**
  * {@inheritDoc}
  */
 public Repository getRepository() throws RepositoryException
 {
  if (repository == null) {
   synchronized (JackrabbitRepositoryAccessor.class) {
    if (repository == null) {
     startRepository();
    }
   }
  }

  return repository;
 }

 /**
  * {@inheritDoc}
  */
 public void startRepository() throws RepositoryException
 {
  try {
   repository = repositoryProvider.get();

   if (repository == null) {
    throw new RepositoryException("Unable to acquire Repository '" + repositoryName
                                  + "' via JNDI");
   }

   if (LOG.isDebugEnabled()) {
    LOG.debug("==> Repository started.");
   }

   // get default workspace (it's always available)
   defaultWorkspace = repository.login().getWorkspace();
   if (LOG.isDebugEnabled()) {
    LOG.debug("==> Default workspace '" + (defaultWorkspace != null ? defaultWorkspace.getName() : "null")
              + "' acquired.");
   }
  } catch (Throwable t) {
   throw new RepositoryException("Unable to acquire Repository '" + repositoryName
                                 + "' via JNDI", t);
  }
 }

 /**
  * {@inheritDoc}
  */
 public Session getSession(final String workspaceName, final String viewerId)
     throws LoginException, NoSuchWorkspaceException, AccessDeniedException, NamespaceException,
            RegisterNodeTypeException, RepositoryException
 {
  if (workspaceName == null) {
   throw new NoSuchWorkspaceException("Workspace name is null. JCR Session can be not opened.");
  }

  Session jcrSession = null;
  Map<String, Session> workspace2Session = THREAD_SESSION.get();
  if (workspace2Session == null) {
   workspace2Session = new HashMap<String, Session>();
  } else {
   jcrSession = workspace2Session.get(workspaceName);
  }

  if (jcrSession != null && !jcrSession.isLive()) {
   jcrSession = null;
  }

  if (jcrSession == null) {
   if (LOG.isDebugEnabled()) {
    LOG.debug("==> Opening new JCR Session for the current thread.");
   }

   SimpleCredentials credentials = new SimpleCredentials(viewerId, "".toCharArray());
   try {
    // authentication to get jcr session
    jcrSession = getRepository().login(credentials, workspaceName);
   } catch (NoSuchWorkspaceException e) {
    // try to create new workspace with the given name because it doesn't exist yet
    Workspace workspace = getDefaultWorkspace();
    if (workspace == null) {
     throw new NoSuchWorkspaceException("Default workspace could be not created. JCR Session can be not opened.");
    }

    if (LOG.isDebugEnabled()) {
     LOG.debug("==> Try to create workspace '" + workspaceName + "'.");
    }

    // create new workspace
    ((JackrabbitWorkspace) workspace).createWorkspace(workspaceName);
    if (LOG.isDebugEnabled()) {
     LOG.debug("==> Workspace '" + workspaceName + "' has been created.");
    }

    // authentication again to get jcr session
    jcrSession = getRepository().login(credentials, workspaceName);
   }

   if (jcrSession == null) {
    throw new LoginException("JCR Session could be not opened (null).");
   }

   workspace2Session.put(workspaceName, jcrSession);
   THREAD_SESSION.set(workspace2Session);
  }

  // register core namespace mapping and node types if they were not registered yet
  if (!isRegistered) {
   synchronized (JackrabbitRepositoryAccessor.class) {
    if (!isRegistered) {
     NamespaceRegistry namespaceRegistry = jcrSession.getWorkspace().getNamespaceRegistry();

     // check whether the namespace prefix or uri already exist
     if (!ArrayUtils.contains(namespaceRegistry.getPrefixes(), Constants.NAMESPACE_PREFIX)
         || !ArrayUtils.contains(namespaceRegistry.getURIs(), Constants.NAMESPACE_URI)) {
      // register namespace
      namespaceRegistry.registerNamespace(Constants.NAMESPACE_PREFIX, Constants.NAMESPACE_URI);
      if (LOG.isDebugEnabled()) {
       LOG.debug("Namespace prefix '" + Constants.NAMESPACE_PREFIX
                 + "' has been registered to the uri '"
                 + Constants.NAMESPACE_URI + "'");
      }
     }

     // register core node types!
     InputStream inputStream = NodeTypeConfig.getInputStreamConfig("core_node_types.xml");
     if (inputStream == null) {
      LOG.error("Node type definition 'core_node_types.xml' was not found");
      throw new RegisterNodeTypeException("Node type definition 'core_node_types.xml' was not found");
     }

     registerNodeType(jcrSession, inputStream);

     if (LOG.isDebugEnabled()) {
      LOG.debug("Register of core node types is ensured");
     }

     isRegistered = true;
    }
   }
  }

  // register core namespace mapping and node types if they were not registered yet
  if (!isCustomRegistered) {
   synchronized (JackrabbitRepositoryAccessor.class) {
    if (!isCustomRegistered) {
     if (!ArrayUtils.isEmpty(customNodeTypeConfigs)) {
      NamespaceRegistry namespaceRegistry = jcrSession.getWorkspace().getNamespaceRegistry();

      for (NodeTypeConfig ndc : customNodeTypeConfigs) {
       if (ndc.getNamespacePrefix() != null && ndc.getNamespaceUri() != null) {
        // check whether the namespace prefix or uri already exist
        if (!ArrayUtils.contains(namespaceRegistry.getPrefixes(), ndc.getNamespacePrefix())
            || !ArrayUtils.contains(namespaceRegistry.getURIs(), ndc.getNamespaceUri())) {
         // register namespace
         namespaceRegistry.registerNamespace(ndc.getNamespacePrefix(),
                                             ndc.getNamespaceUri());
         if (LOG.isDebugEnabled()) {
          LOG.debug("Custom namespace prefix '" + ndc.getNamespacePrefix()
                    + "' has been registered to the custom uri '"
                    + ndc.getNamespaceUri() + "'");
         }
        }
       }

       if (ndc.getInputStream() != null) {
        registerNodeType(jcrSession, ndc.getInputStream());
       }
      }

      if (LOG.isDebugEnabled()) {
       LOG.debug("Register of " + customNodeTypeConfigs.length + " custom node types is ensured");
      }
     }

     isCustomRegistered = true;
    }
   }
  }

  return jcrSession;
 }

 /**
  * {@inheritDoc}
  */
 public void releaseSession()
 {
  Map<String, Session> workspace2Session = THREAD_SESSION.get();
  if (workspace2Session != null) {
   Collection<Session> sessions = workspace2Session.values();
   for (Session jcrSession : sessions) {
    if (jcrSession != null && jcrSession.isLive()) {
     if (LOG.isDebugEnabled()) {
      LOG.debug("==> Closing JCR Session for the current thread.");
     }

     jcrSession.logout();
    }
   }
  }

  THREAD_SESSION.set(null);
 }

 /**
  * {@inheritDoc}
  */
 public void releaseRepository()
 {
  // Jackrabbit specific
  if (repository instanceof JackrabbitRepository) {
   ((JackrabbitRepository) repository).shutdown();
  }

  repository = null;
 }
}
RepositoryAccessor should be accessible from application scope and can be instantiated during application startup (e.g. in ServletContextListener's contextInitialized() or in an JSF managed bean's method annotated with @PostConstruct). Well. Let's put all classes together! I would like to show typically steps to get an instance of JackrabbitRepositoryAccessor.
// create a google guice injector for the configuration module
Injector injector = Guice.createInjector(new DefaultConfigurationGuiceModule());

// create the factory instance to create a repository accessor instance
RepositoryAccessorFactory repositoryAccessorFactory = injector.getInstance(RepositoryAccessorFactory.class);

// create custom node type configurations from describing XML file and given namespace prefix / URI
NodeTypeConfig[] nodeTypeConfigs = new NodeTypeConfig[1];
nodeTypeConfigs[0] = NodeTypeConfig.getNodeTypeConfig("custom_node.xml", "xyz", "http://mysite.net/xyz");

// create an instance of repository accessor (parameter can be null if no custom node types are available)
repositoryAccessor = repositoryAccessorFactory.create(nodeTypeConfigs);

// method and field injection
injector.injectMembers(repositoryAccessor);

// start and initialize the content repository
repositoryAccessor.startRepository();
Now you can access both - Repository and JCR Session somewhere you want
javax.jcr.Repository repository = repositoryAccessor.getRepository();
javax.jcr.Session session = repositoryAccessor.getSession(workspaceName, viewerId);
Not forget to release repository when the application goes down (e.g. in ServletContextListener's contextDestroyed() or in an JSF managed bean's method annotated with @PreDestroy).
repositoryAccessor.releaseRepository();
repositoryAccessor = null;
That's all :-)

No comments:

Post a Comment

Note: Only a member of this blog may post a comment.