/* * Licensed to the Apache Software Foundation (ASF) under one or more * contributor license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright ownership. * The ASF licenses this file to You 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.apache.jackrabbit.core.retention; import org.apache.commons.io.IOUtils; import javax.jcr.retention.Hold; import javax.jcr.retention.RetentionPolicy; import org.apache.jackrabbit.core.id.NodeId; import org.apache.jackrabbit.core.NodeImpl; import org.apache.jackrabbit.core.id.PropertyId; import org.apache.jackrabbit.core.PropertyImpl; import org.apache.jackrabbit.core.SessionImpl; import org.apache.jackrabbit.core.fs.FileSystem; import org.apache.jackrabbit.core.fs.FileSystemException; import org.apache.jackrabbit.core.fs.FileSystemResource; import org.apache.jackrabbit.core.observation.SynchronousEventListener; import org.apache.jackrabbit.spi.Name; import org.apache.jackrabbit.spi.Path; import org.apache.jackrabbit.spi.commons.name.PathMap; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.jcr.RepositoryException; import javax.jcr.Workspace; import javax.jcr.observation.Event; import javax.jcr.observation.EventIterator; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.util.Arrays; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Set; /** * <code>RetentionEvaluatorImpl</code>... */ public class RetentionRegistryImpl implements RetentionRegistry, SynchronousEventListener { /** * logger instance */ private static final Logger log = LoggerFactory.getLogger(RetentionRegistryImpl.class); /** * Name of the file storing the existing retention/holds */ private static final String FILE_NAME = "retention"; private final PathMap<RetentionPolicyImpl> retentionMap = new PathMap<RetentionPolicyImpl>(); private final PathMap<List<HoldImpl>> holdMap = new PathMap<List<HoldImpl>>(); private final SessionImpl session; private final FileSystemResource retentionFile; private long holdCnt; private long retentionCnt; private boolean initialized; public RetentionRegistryImpl(SessionImpl session, FileSystem fs) throws RepositoryException { this.session = session; this.retentionFile = new FileSystemResource(fs, FileSystem.SEPARATOR + FILE_NAME); // start listening to added/changed or removed holds and retention policies. Workspace wsp = session.getWorkspace(); // register event listener to be informed about new/removed holds and // retention policies. int types = Event.PROPERTY_ADDED | Event.PROPERTY_REMOVED | Event.PROPERTY_CHANGED; String[] ntFilter = new String[] {session.getJCRName(RetentionManagerImpl.REP_RETENTION_MANAGEABLE)}; wsp.getObservationManager().addEventListener(this, types, "/", true, null, ntFilter, false); // populate the retentionMap and the holdMap with the effective // holds and retention policies present within the content. try { readRetentionFile(); } catch (FileSystemException e) { throw new RepositoryException("Error while reading retention/holds from '" + retentionFile.getPath() + "'", e); } catch (IOException e) { throw new RepositoryException("Error while reading retention/holds from '" + retentionFile.getPath() + "'", e); } initialized = true; } /** * Read the file system resource containing the node ids of those nodes * contain holds/retention policies and populate the 2 path maps. * * If an entry in the retention file doesn't have a corresponding entry * (either rep:hold property or rep:retentionPolicy property at the * node identified by the node id entry) or doesn't correspond to an existing * node, that entry will be ignored. Upon {@link #close()} of this * manager, the file will be updated to reflect the actual set of holds/ * retentions present and effective in the content. * * @throws IOException * @throws FileSystemException */ private void readRetentionFile() throws IOException, FileSystemException { if (retentionFile.exists()) { BufferedReader reader = null; try { reader = new BufferedReader(new InputStreamReader(retentionFile.getInputStream())); String line; while ((line = reader.readLine()) != null) { NodeId nodeId = NodeId.valueOf(line); try { NodeImpl node = (NodeImpl) session.getItemManager().getItem(nodeId); Path nodePath = node.getPrimaryPath(); if (node.hasProperty(RetentionManagerImpl.REP_HOLD)) { PropertyImpl prop = node.getProperty(RetentionManagerImpl.REP_HOLD); addHolds(nodePath, prop); } if (node.hasProperty(RetentionManagerImpl.REP_RETENTION_POLICY)) { PropertyImpl prop = node.getProperty(RetentionManagerImpl.REP_RETENTION_POLICY); addRetentionPolicy(nodePath, prop); } } catch (RepositoryException e) { // node doesn't exist any more or hold/retention has been removed. // ignore. upon close() the file will not contain the given nodeId // any more. log.warn("Unable to read retention policy / holds from node '" + nodeId + "': " + e.getMessage()); } } } finally { IOUtils.closeQuietly(reader); } } } /** * Write back the file system resource containing the node ids of those * nodes containing holds and/or retention policies. Each node id is * present only once. */ private void writeRetentionFile() { final Set<NodeId> nodeIds = new HashSet<NodeId>(); // first look for nodes containing holds holdMap.traverse(new PathMap.ElementVisitor<List<HoldImpl>>() { public void elementVisited(PathMap.Element<List<HoldImpl>> element) { List<HoldImpl> holds = element.get(); if (!holds.isEmpty()) { nodeIds.add(holds.get(0).getNodeId()); } } }, false); // then collect ids of nodes having an retention policy retentionMap.traverse(new PathMap.ElementVisitor<RetentionPolicyImpl>() { public void elementVisited(PathMap.Element<RetentionPolicyImpl> element) { nodeIds.add(element.get().getNodeId()); } }, false); if (!nodeIds.isEmpty()) { BufferedWriter writer = null; try { writer = new BufferedWriter(new OutputStreamWriter(retentionFile.getOutputStream())); for (Iterator<NodeId> it = nodeIds.iterator(); it.hasNext();) { writer.write(it.next().toString()); if (it.hasNext()) { writer.newLine(); } } } catch (FileSystemException fse) { log.error("Error while saving locks to '" + retentionFile.getPath() + "': " + fse.getMessage()); } catch (IOException ioe) { log.error("Error while saving locks to '" + retentionFile.getPath() + "': " + ioe.getMessage()); } finally { IOUtils.closeQuietly(writer); } } } public void close() { writeRetentionFile(); initialized = false; } private void addHolds(Path nodePath, PropertyImpl p) throws RepositoryException { synchronized (holdMap) { HoldImpl[] holds = HoldImpl.createFromProperty(p, ((PropertyId) p.getId()).getParentId()); holdMap.put(nodePath, Arrays.asList(holds)); holdCnt++; } } private void removeHolds(Path nodePath) { synchronized (holdMap) { PathMap.Element<List<HoldImpl>> el = holdMap.map(nodePath, true); if (el != null) { el.remove(); holdCnt--; } // else: no entry for holds on nodePath (should not occur) } } private void addRetentionPolicy(Path nodePath, PropertyImpl p) throws RepositoryException { synchronized (retentionMap) { RetentionPolicyImpl rp = new RetentionPolicyImpl( p.getString(), ((PropertyId) p.getId()).getParentId(), session); retentionMap.put(nodePath, rp); retentionCnt++; } } private void removeRetentionPolicy(Path nodePath) { synchronized (retentionMap) { PathMap.Element<RetentionPolicyImpl> el = retentionMap.map(nodePath, true); if (el != null) { el.remove(); retentionCnt--; } // else: no entry for holds on nodePath (should not occur) } } //--------------------------------------------------< RetentionRegistry >--- /** * @see RetentionRegistry#hasEffectiveHold(org.apache.jackrabbit.spi.Path,boolean) */ public boolean hasEffectiveHold(Path nodePath, boolean checkParent) throws RepositoryException { if (!initialized) { throw new IllegalStateException("Not initialized."); } if (holdCnt <= 0) { return false; } PathMap.Element<List<HoldImpl>> element = holdMap.map(nodePath, false); List<HoldImpl> holds = element.get(); if (holds != null) { if (element.hasPath(nodePath)) { // one or more holds on the specified path return true; } else if (checkParent && !nodePath.denotesRoot() && element.hasPath(nodePath.getAncestor(1))) { // hold present on the parent node without checking for being // a deep hold. // this required for removal of a node that can be inhibited // by a hold on the node itself, by a hold on the parent or // by a deep hold on any ancestor. return true; } else { for (Hold hold : holds) { if (hold.isDeep()) { return true; } } } } // no hold at path or no deep hold on parent. return false; } /** * @see RetentionRegistry#hasEffectiveRetention(org.apache.jackrabbit.spi.Path,boolean) */ public boolean hasEffectiveRetention(Path nodePath, boolean checkParent) throws RepositoryException { if (!initialized) { throw new IllegalStateException("Not initialized."); } if (retentionCnt <= 0) { return false; } RetentionPolicy rp = null; PathMap.Element<RetentionPolicyImpl> element = retentionMap.map(nodePath, true); if (element != null) { rp = element.get(); } if (rp == null && checkParent && (!nodePath.denotesRoot())) { element = retentionMap.map(nodePath.getAncestor(1), true); if (element != null) { rp = element.get(); } } return rp != null; } //-------------------------------------------< SynchronousEventListener >--- /** * @param events Events reporting hold/retention policy changes. */ public void onEvent(EventIterator events) { while (events.hasNext()) { Event ev = events.nextEvent(); try { Path evPath = session.getQPath(ev.getPath()); Path nodePath = evPath.getAncestor(1); Name propName = evPath.getName(); if (RetentionManagerImpl.REP_HOLD.equals(propName)) { // hold changes switch (ev.getType()) { case Event.PROPERTY_ADDED: case Event.PROPERTY_CHANGED: // build the Hold objects from the rep:hold property // and put them into the hold map. PropertyImpl p = (PropertyImpl) session.getProperty(ev.getPath()); addHolds(nodePath, p); break; case Event.PROPERTY_REMOVED: // all holds present on this node were remove // -> remove the corresponding entry in the holdMap. removeHolds(nodePath); break; } } else if (RetentionManagerImpl.REP_RETENTION_POLICY.equals(propName)) { // retention policy changes switch (ev.getType()) { case Event.PROPERTY_ADDED: case Event.PROPERTY_CHANGED: // build the RetentionPolicy objects from the rep:retentionPolicy property // and put it into the retentionMap. PropertyImpl p = (PropertyImpl) session.getProperty(ev.getPath()); addRetentionPolicy(nodePath, p); break; case Event.PROPERTY_REMOVED: // retention policy present on this node was remove // -> remove the corresponding entry in the retentionMap. removeRetentionPolicy(nodePath); break; } } // else: not interested in any other property -> ignore. } catch (RepositoryException e) { log.warn("Internal error while processing event. {}", e.getMessage()); // ignore. } } } }