/** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.brixcms.workspace; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.Node; import javax.jcr.NodeIterator; import javax.jcr.Property; import javax.jcr.PropertyIterator; import javax.jcr.RepositoryException; import javax.jcr.Session; import javax.jcr.lock.LockException; import javax.jcr.observation.Event; import javax.jcr.observation.EventIterator; import javax.jcr.observation.EventListener; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Set; import java.util.UUID; public abstract class AbstractClusteredWorkspaceManager extends AbstractWorkspaceManager implements ClusteredWorkspaceManager { private static final Logger log = LoggerFactory.getLogger(AbstractClusteredWorkspaceManager.class); private Long lockTimeoutHint = 360L; private String ownerInfo = "Locked by AbstractClusteredWorkspaceManager"; // property used to mark deleted workspace that couldn't have all nodes // deleted // such workspace can only be deleted manually when all nodes are down private final String PROPERTY_DO_NOT_USE = "doNotUse"; private final Set<String> availableWorkspaceNames = new HashSet<String>(); private final Set<String> deletedWorkspaceNames = new HashSet<String>(); private final Map<String, Session> workspaceToSessionMap = new HashMap<String, Session>(); public AbstractClusteredWorkspaceManager() { } public void workspaceCreated(String workspaceId) { // register the listener if (isBrixWorkspace(workspaceId)) { getSession(workspaceId); } } public synchronized List<Workspace> getWorkspaces() { List<Workspace> result = new ArrayList<Workspace>(); for (String s : availableWorkspaceNames) { result.add(getWorkspace(s)); } return result; } public synchronized Workspace createWorkspace() { try { // either try to restore deleted workspace for (String s : deletedWorkspaceNames) { if (restoreWorkspace(s)) { return getWorkspace(s); } } // or create new one String id = getWorkspaceId(UUID.randomUUID()); createWorkspace(id); Session session = getSession(id); Node node = session.getRootNode().addNode(NODE_NAME, "nt:unstructured"); node.addMixin("mix:lockable"); node.addNode(PROPERTIES_NODE, "nt:unstructured"); availableWorkspaceNames.add(id); session.save(); return getWorkspace(id); } catch (RepositoryException e) { throw new JcrException(e); } } public synchronized boolean workspaceExists(String workspaceId) { return availableWorkspaceNames.contains(workspaceId); } abstract protected void createWorkspace(String workspaceId); @Override protected void delete(String workspaceId) throws RepositoryException { synchronized (this) { if (!availableWorkspaceNames.contains(workspaceId)) { throw new IllegalStateException("Workspace " + workspaceId + " either does not exist or was already deleted."); } } Session session = getSession(workspaceId); Node node = (Node) session.getItem(NODE_PATH); tryLockNode(node); try { synchronized (this) { availableWorkspaceNames.remove(workspaceId); removeCachedWorkspaceAttributes(workspaceId); node.setProperty(DELETED_PROPERTY, true); if (node.hasNode(PROPERTIES_NODE)) { node.getNode(PROPERTIES_NODE).remove(); } node.getSession().save(); } try { cleanWorkspace(session); session.save(); } catch (RepositoryException e) { // problem deleting nodes node.setProperty(PROPERTY_DO_NOT_USE, true); node.getSession().save(); throw (e); } synchronized (this) { deletedWorkspaceNames.add(workspaceId); } } finally { node.getSession().getWorkspace().getLockManager().unlock(node.getPath()); } } private synchronized void tryLockNode(Node node) throws RepositoryException { int sleep = 1000; for (int i = 0; i < 10; ++i) { if (!node.isLocked()) { try { node.getSession().getWorkspace().getLockManager().lock(node.getPath(), false, true, lockTimeoutHint, ownerInfo); return; } catch (LockException e) { } } try { log.info("Node already locked, waiting..."); Thread.sleep(sleep); } catch (InterruptedException e) { } } throw new RuntimeException("Couldn't lock " + node.getPath()); } private void cleanWorkspace(Session session) throws RepositoryException { Node root = session.getRootNode(); NodeIterator iterator = root.getNodes(); while (iterator.hasNext()) { Node node = iterator.nextNode(); if (!node.getName().equals(NODE_NAME) && !node.getName().equals("jcr:system")) { node.remove(); } } } @Override protected synchronized String getAttribute(String workspaceId, String key) { if (!availableWorkspaceNames.contains(workspaceId)) { throw new IllegalStateException("Trying to get attribute of workspace " + workspaceId + " that doesn't exist or was removed."); } return getCachedAttribute(workspaceId, key); } @Override protected Iterator<String> getAttributeKeys(String workspaceId) { if (!availableWorkspaceNames.contains(workspaceId)) { throw new IllegalStateException("Trying to get attribute keys of workspace " + workspaceId + " that doesn't exist or was removed."); } return getCachedAttributeKeys(workspaceId); } public synchronized AbstractClusteredWorkspaceManager initialize() { try { List<String> accessibleWorkspaces = new ArrayList<String>(getAccessibleWorkspaceIds()); // loop until all workspaces are processed or there were 20 attempts for (int i = 0; i < 20 && !accessibleWorkspaces.isEmpty(); ++i) { initialize(accessibleWorkspaces); if (!accessibleWorkspaces.isEmpty()) { int wait = 1000; log.info("Couldn't read all workspaces, some of them were locked, waiting " + wait + " milliseconds."); try { Thread.sleep(wait); } catch (InterruptedException e) { } } } if (!accessibleWorkspaces.isEmpty()) { log.info("Some workspaces couldn't be read during initialization (they were locked)."); } return this; } catch (RepositoryException e) { throw new JcrException(e); } } abstract protected List<String> getAccessibleWorkspaceIds(); /** * Iterates over the list of workspaces gathering information about each workspace. Every processed workspace is * removed from the list. Workspaces left in list could not have been locked properly. * * @param accessibleWorkspaces * @throws RepositoryException */ private void initialize(List<String> accessibleWorkspaces) throws RepositoryException { for (Iterator<String> i = accessibleWorkspaces.iterator(); i.hasNext();) { String workspace = i.next(); if (isBrixWorkspace(workspace)) { Session session = getSession(workspace); if (session.itemExists(NODE_PATH)) { Node node = (Node) session.getItem(NODE_PATH); // fix the node if it is not lockable if (!node.isNodeType("mix:lockable")) { node.addMixin("mix:lockable"); node.getSession().save(); } // ignore workspaces in which the node is either locked or // can not be locked if (node.isLocked()) { continue; } try { node.getSession().getWorkspace().getLockManager().lock(node.getPath(), false, true, lockTimeoutHint, ownerInfo); } catch (LockException e) { continue; } try { // determine whether the workspace is deleted or // available if (node.hasProperty(DELETED_PROPERTY) && node.getProperty(DELETED_PROPERTY).getBoolean() == true) { deletedWorkspaceNames.add(workspace); } else { // for available workspaces read the properties availableWorkspaceNames.add(workspace); if (node.hasNode(PROPERTIES_NODE)) { Node properties = node.getNode(PROPERTIES_NODE); PropertyIterator iterator = properties.getProperties(); while (iterator.hasNext()) { Property property = iterator.nextProperty(); setCachedAttribute(workspace, property.getName(), property.getValue().getString()); } } } } finally { node.getSession().getWorkspace().getLockManager().unlock(node.getPath()); } } } i.remove(); } } synchronized Session getSession(String workspaceId) { try { Session session = workspaceToSessionMap.get(workspaceId); if (session == null) { session = createSession(workspaceId); EventListener listener = new SessionEventListener(session); int events = Event.NODE_ADDED | Event.PROPERTY_ADDED | Event.PROPERTY_CHANGED | Event.PROPERTY_REMOVED; session.getWorkspace().getObservationManager().addEventListener(listener, events, "/", true, null, null, true); // we need to keep the sessions opened otherwise the listeners will be removed workspaceToSessionMap.put(workspaceId, session); } return session; } catch (RepositoryException e) { throw new JcrException(e); } } abstract protected Session createSession(String workspaceId); /** * Tries to restore the deleted workspace. * * @param workspaceId * @return <code>true</code> if the workspace was succesfully restored, <code>false</code> otherwise * @throws RepositoryException */ private boolean restoreWorkspace(String workspaceId) throws RepositoryException { Session s = getSession(workspaceId); Node node = (Node) s.getItem(NODE_PATH); try { if (node.isLocked()) { return false; } node.getSession().getWorkspace().getLockManager().lock(node.getPath(), false, false, lockTimeoutHint, ownerInfo); try { // if the workspace is still deleted if (node.hasProperty(DELETED_PROPERTY) && node.getProperty(DELETED_PROPERTY).getBoolean() == true && (!node.hasProperty(PROPERTY_DO_NOT_USE) || node.getProperty(PROPERTY_DO_NOT_USE) .getBoolean() == false)) { node.setProperty(DELETED_PROPERTY, (String) null); availableWorkspaceNames.add(workspaceId); deletedWorkspaceNames.remove(workspaceId); // clear properties if there are any if (node.hasNode(PROPERTIES_NODE)) { node.getNode(PROPERTIES_NODE).remove(); } node.addNode(PROPERTIES_NODE, "nt:unstructured"); s.save(); return false; } else { return false; } } finally { node.getSession().getWorkspace().getLockManager().unlock(node.getPath()); } } catch (LockException e) { return false; } } @Override protected synchronized void setAttribute(String workspaceId, String attributeKey, String attributeValue) throws RepositoryException { if (!availableWorkspaceNames.contains(workspaceId)) { throw new IllegalStateException("Trying to set attribute of workspace " + workspaceId + " that doesn't exist or was removed."); } Session session = getSession(workspaceId); Node node = (Node) session.getItem(NODE_PATH); Node properties; if (!node.hasNode(PROPERTIES_NODE)) { properties = node.addNode(PROPERTIES_NODE, "nt:unstructured"); } else { properties = node.getNode(PROPERTIES_NODE); } properties.setProperty(attributeKey, attributeValue); node.getSession().save(); setCachedAttribute(workspaceId, attributeKey, attributeValue); } private class SessionEventListener implements EventListener { private final Session session; public SessionEventListener(Session session) { this.session = session; } public void onEvent(EventIterator events) { synchronized (AbstractClusteredWorkspaceManager.this) { while (events.hasNext()) { processEvent(events.nextEvent()); } } } private void workspaceCreated() { String name = session.getWorkspace().getName(); availableWorkspaceNames.add(name); deletedWorkspaceNames.remove(name); } private void workspaceRemoved() { String name = session.getWorkspace().getName(); availableWorkspaceNames.remove(name); deletedWorkspaceNames.add(name); removeCachedWorkspaceAttributes(name); } private void attributeChanged(String key, String value) { String name = session.getWorkspace().getName(); setCachedAttribute(name, key, value); } public void processEvent(Event event) { try { final String deletedPropertyPath = NODE_PATH + "/" + DELETED_PROPERTY; final String propertiesPath = NODE_PATH + "/" + PROPERTIES_NODE + "/"; final String path = event.getPath(); if (event.getType() == Event.PROPERTY_REMOVED) { if (path.equals(deletedPropertyPath)) { // deleted property of brix:workspace removed -> workspace was restored workspaceCreated(); } if (path.startsWith(propertiesPath)) { // removed a property attributeChanged(path.substring(propertiesPath.length()), null); } } else if (event.getType() == Event.PROPERTY_ADDED || event.getType() == Event.PROPERTY_CHANGED) { Property property = (Property) session.getItem(path); // deleted property of brix:workspace if (path.equals(deletedPropertyPath)) { if (property.getValue().getBoolean() == true) { workspaceRemoved(); } else { workspaceCreated(); } } else if (path.startsWith(propertiesPath)) { attributeChanged(property.getName(), property.getValue().getString()); } } else if (event.getType() == Event.NODE_ADDED) { // new workspace - created brix:workspace node if (path.equals(NODE_PATH)) { workspaceCreated(); } } } catch (RepositoryException e) { log.warn("Error processing event ", e); } } } }