| /* |
| * 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.sling.jcr.maintenance.internal; |
| |
| import java.util.ArrayList; |
| import java.util.Collections; |
| import java.util.List; |
| import java.util.Optional; |
| |
| import javax.jcr.ItemNotFoundException; |
| import javax.jcr.Node; |
| import javax.jcr.PathNotFoundException; |
| import javax.jcr.RepositoryException; |
| import javax.jcr.Session; |
| import javax.jcr.version.Version; |
| import javax.jcr.version.VersionHistory; |
| import javax.jcr.version.VersionIterator; |
| import javax.jcr.version.VersionManager; |
| import javax.management.DynamicMBean; |
| |
| import org.apache.jackrabbit.oak.commons.jmx.AnnotatedStandardMBean; |
| import org.apache.sling.api.resource.LoginException; |
| import org.apache.sling.api.resource.Resource; |
| import org.apache.sling.api.resource.ResourceResolver; |
| import org.apache.sling.api.resource.ResourceResolverFactory; |
| import org.apache.sling.jcr.maintenance.VersionCleanupConfig; |
| import org.osgi.service.component.annotations.Activate; |
| import org.osgi.service.component.annotations.Component; |
| import org.osgi.service.component.annotations.ConfigurationPolicy; |
| import org.osgi.service.component.annotations.Reference; |
| import org.osgi.service.component.annotations.ReferenceCardinality; |
| import org.osgi.service.component.annotations.ReferencePolicyOption; |
| import org.osgi.service.metatype.annotations.Designate; |
| import org.slf4j.Logger; |
| import org.slf4j.LoggerFactory; |
| |
| /** |
| * |
| */ |
| @Component(service = { VersionCleanupMBean.class, Runnable.class, DynamicMBean.class }, property = { |
| "jmx.objectname=org.apache.sling.jcr.maintenance:type=VersionCleanup", |
| "scheduler.concurrent:Boolean=false" }, configurationPolicy = ConfigurationPolicy.REQUIRE, immediate = true) |
| @Designate(ocd = VersionCleanupConfig.class) |
| public class VersionCleanup extends AnnotatedStandardMBean implements Runnable, VersionCleanupMBean { |
| |
| private static final Logger log = LoggerFactory.getLogger(VersionCleanup.class); |
| |
| private Thread cleanupThread; |
| private final ResourceResolverFactory factory; |
| private long lastCleanedVersions; |
| private String lastFailureMessage; |
| private final List<VersionCleanupPath> versionCleanupConfigs; |
| |
| @Activate |
| public VersionCleanup( |
| @Reference(cardinality = ReferenceCardinality.AT_LEAST_ONE, policyOption = ReferencePolicyOption.GREEDY) final List<VersionCleanupPath> versionCleanupConfigs, |
| @Reference final ResourceResolverFactory factory) { |
| super(VersionCleanupMBean.class); |
| this.factory = factory; |
| this.versionCleanupConfigs = versionCleanupConfigs; |
| versionCleanupConfigs.sort((c1, c2) -> c1.getPath().compareTo(c2.getPath()) * -1); |
| |
| } |
| |
| private String getPath(final Session session, final VersionHistory versionHistory) throws RepositoryException { |
| String identifier = versionHistory.getVersionableIdentifier(); |
| try { |
| Node versionableNode = session.getNodeByIdentifier(identifier); |
| return versionableNode.getPath(); |
| } catch (ItemNotFoundException infe) { |
| log.debug("Unable to get versionable node by ID: {}, exception: {}", identifier, infe.getMessage()); |
| return versionHistory.getProperty(session.getWorkspace().getName()).getString(); |
| } |
| } |
| |
| private void cleanupVersions(final Session session, final Resource history) { |
| try { |
| final VersionHistory versionHistory = (VersionHistory) session.getItem(history.getPath()); |
| final String path = getPath(session, versionHistory); |
| final VersionCleanupPath config = VersionCleanupPath.getMatchingConfiguration(this.versionCleanupConfigs, |
| path); |
| int limit = config.getLimit(); |
| |
| if (!isMatchingVersion(session, path, versionHistory) && !config.isKeepVersions() && limit > 0) { |
| log.debug("Deleted, removing all but last version"); |
| limit = 1; |
| } |
| log.debug("Cleaning up versions for: {}", versionHistory.getPath()); |
| final VersionIterator versionIterator = versionHistory.getAllVersions(); |
| final List<String> versionNames = new ArrayList<>(); |
| while (versionIterator.hasNext()) { |
| final Version version = versionIterator.nextVersion(); |
| if (!version.getName().equals("jcr:rootVersion")) { |
| versionNames.add(version.getName()); |
| } |
| } |
| if (versionNames.size() > limit) { |
| final List<String> toCleanup = versionNames.subList(0, versionNames.size() - limit); |
| log.info("Cleaning up {} versions from {} at: {}", toCleanup.size(), path, versionHistory.getPath()); |
| for (final String item : toCleanup) { |
| versionHistory.removeVersion(item); |
| log.trace("Cleaned up: {}", item); |
| lastCleanedVersions++; |
| } |
| } |
| } catch (final RepositoryException re) { |
| log.warn("Failed to cleanup version history for: {}", history.getPath(), re); |
| } |
| |
| } |
| |
| private boolean findVersions(final Session session, final Resource resource) throws RepositoryException { |
| if (Thread.interrupted()) { |
| return true; |
| } |
| log.debug("Finding versions under: {}", resource.getPath()); |
| if ("nt:versionHistory".equals(resource.getResourceType())) { |
| resource.getResourceResolver().refresh(); |
| cleanupVersions(session, resource); |
| } else { |
| for (final Resource child : resource.getChildren()) { |
| if (findVersions(session, child)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| private boolean isMatchingVersion(Session session, String path, VersionHistory versionHistory) |
| throws RepositoryException { |
| try { |
| VersionManager versionManager = session.getWorkspace().getVersionManager(); |
| String baseVersionPath = versionManager.getBaseVersion(path).getParent().getPath(); |
| String versionHistoryPath = versionHistory.getPath(); |
| |
| return session.nodeExists(path) && isVersionable(session.getNode(path)) |
| && baseVersionPath.equals(versionHistoryPath); |
| } catch (PathNotFoundException pnfe) { |
| log.debug("Path: {} not found: {}", path, pnfe.getMessage()); |
| return false; |
| } |
| } |
| |
| private boolean isVersionable(final Node node) throws RepositoryException { |
| return node != null && node.isNodeType("{http://www.jcp.org/jcr/mix/1.0}versionable"); |
| } |
| |
| @Override |
| public void run() { |
| if (isRunning()) { |
| log.warn("Version cleanup already running!"); |
| } else { |
| cleanupThread = new Thread((this::doRun)); |
| cleanupThread.setDaemon(true); |
| cleanupThread.start(); |
| } |
| } |
| |
| private void doRun() { |
| log.info("Running version cleanup"); |
| lastCleanedVersions = 0; |
| try { |
| try (final ResourceResolver adminResolver = factory.getServiceResourceResolver( |
| Collections.singletonMap(ResourceResolverFactory.SUBSERVICE, "sling-versionmgr"))) { |
| final Resource versionRoot = adminResolver.getResource("/jcr:system/jcr:versionStorage"); |
| final Session session = Optional.ofNullable(versionRoot.getResourceResolver().adaptTo(Session.class)) |
| .orElseThrow(() -> new RepositoryException("Failed to get session")); |
| for (final Resource folder : versionRoot.getChildren()) { |
| log.info("Traversing and cleaning: {}", folder.getPath()); |
| if (findVersions(session, folder)) { |
| break; |
| } |
| } |
| lastFailureMessage = null; |
| } |
| } catch (final LoginException le) { |
| log.error("Failed to run version cleanup, cannot get service user", le); |
| lastFailureMessage = "Failed to run version cleanup, cannot get service user"; |
| } catch (final RepositoryException re) { |
| log.error("Failed to run version cleanup", re); |
| lastFailureMessage = "Failed to run version cleanup"; |
| } |
| } |
| |
| @Override |
| public boolean isRunning() { |
| return cleanupThread != null && cleanupThread.isAlive(); |
| } |
| |
| @Override |
| public boolean isFailed() { |
| return lastFailureMessage != null; |
| } |
| |
| @Override |
| public String getLastMessage() { |
| return lastFailureMessage; |
| } |
| |
| @Override |
| public long getLastCleanedVersionsCount() { |
| return lastCleanedVersions; |
| } |
| |
| @Override |
| public void start() { |
| this.run(); |
| } |
| |
| @Override |
| public void stop() { |
| Optional.ofNullable(cleanupThread).ifPresent(Thread::interrupt); |
| } |
| } |