/* * JBoss, Home of Professional Open Source. * Copyright 2013, Red Hat, Inc., and individual contributors * as indicated by the @author tags. See the copyright.txt file in the * distribution for a full listing of individual contributors. * * This is free software; you can redistribute it and/or modify it * under the terms of the GNU Lesser General Public License as * published by the Free Software Foundation; either version 2.1 of * the License, or (at your option) any later version. * * This software is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this software; if not, write to the Free * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA * 02110-1301 USA, or see the FSF site: http://www.fsf.org. */ package org.jboss.as.patching.tool; import static org.jboss.as.patching.Constants.BASE; import static org.jboss.as.patching.metadata.Patch.PatchType.CUMULATIVE; import static org.jboss.as.patching.metadata.Patch.PatchType.ONE_OFF; import java.io.File; import java.io.IOException; import java.util.Collections; import java.util.HashMap; import java.util.Map; import java.util.NoSuchElementException; import org.jboss.as.patching.Constants; import org.jboss.as.patching.PatchingException; import org.jboss.as.patching.installation.InstalledIdentity; import org.jboss.as.patching.installation.PatchableTarget; import org.jboss.as.patching.installation.PatchableTarget.TargetInfo; import org.jboss.as.patching.logging.PatchLogger; import org.jboss.as.patching.metadata.Patch; import org.jboss.as.patching.metadata.Patch.PatchType; import org.jboss.as.patching.metadata.PatchBuilder; import org.jboss.as.patching.metadata.PatchElement; import org.jboss.as.patching.metadata.PatchXml; import org.jboss.as.patching.metadata.RollbackPatch; import org.jboss.as.patching.runner.PatchUtils; import org.jboss.dmr.ModelNode; /** * Provides a read-only view to the patching history of the installation. * * @author Alexey Loubyansky */ public interface PatchingHistory { /** * Information about a patch. * * @author Alexey Loubyansky */ public interface Entry { String getPatchId(); Patch.PatchType getType(); String getAppliedAt(); /** * Patch element ids by layer names they are applied to. * * @return map of patch element ids by layer names they are applied to */ Map<String,String> getLayerPatches(); Map<String,String> getAddOnPatches(); Patch getMetadata(); } public interface Iterator extends java.util.Iterator<Entry> { /** * Whether there is still a cumulative patch. * * @return true if there is still a cumulative patch, otherwise - false */ boolean hasNextCP(); /** * Skips all the one-off patches (if any) and moves straight to * the next cumulative patch. If there is no cumulative patch left, * NoSuchElementException will be thrown. * * @return next cumulative patch or throw NoSuchElementException * if no more cumulative patches left */ Entry nextCP(); } /** * Returns the history as a list of ModelNode's * Entry node has the following attributes: * - patch-id - the id of the patch; * - type - the type of the patch (cumulative or one-off); * - applied-at - a timestamp the patch was applied at. * * @return returns a list of entries representing basic info * about the patches applied or an empty list if * there is no patching information * @throws PatchingException in case there was an error loading the history */ ModelNode getHistory() throws PatchingException; /** * Same as getHistory() but for the specified target, * i.e. specific point. * * @param info the point from which to load the history * @return returns a list of entries representing basic info * about the patches applied or an empty list if * there is no patching information * @throws PatchingException in case there was an error loading the history */ ModelNode getHistory(PatchableTarget.TargetInfo info) throws PatchingException; /** * Returns an iterator over the history. * * @return iterator over the patching history * * @throws PatchingException in case there was an error loading the history */ Iterator iterator() throws PatchingException; /** * Same as iterator() but starting from a specific point. * * @param info the point to start from * @return iterator over the patching history * @throws PatchingException in case there was an error loading the history */ Iterator iterator(final PatchableTarget.TargetInfo info) throws PatchingException; public class Factory { private Factory() {} public static ModelNode getHistory(InstalledIdentity installedImage, PatchableTarget.TargetInfo info) throws PatchingException { final ModelNode result = new ModelNode(); result.setEmptyList(); fillHistoryIn(installedImage, info, result); return result; } public static Iterator iterator(final InstalledIdentity mgr, final PatchableTarget.TargetInfo info) { if(info == null) { throw new IllegalArgumentException("target info is null"); } return new IteratorImpl(info, mgr); } public static PatchingHistory getHistory(final InstalledIdentity installedIdentity) { if(installedIdentity == null) { throw new IllegalStateException("installedImage is null"); } return new PatchingHistory() { @Override public ModelNode getHistory() throws PatchingException { try { return getHistory(installedIdentity.getIdentity().loadTargetInfo()); } catch (IOException e) { throw new PatchingException(PatchLogger.ROOT_LOGGER.failedToLoadInfo(installedIdentity.getIdentity().getName()), e); } } @Override public ModelNode getHistory(TargetInfo info) throws PatchingException { return Factory.getHistory(installedIdentity, info); } @Override public Iterator iterator() throws PatchingException { try { return iterator(installedIdentity.getIdentity().loadTargetInfo()); } catch (IOException e) { throw new PatchingException(PatchLogger.ROOT_LOGGER.failedToLoadInfo(installedIdentity.getIdentity().getName()), e); } } @Override public Iterator iterator(TargetInfo info) throws PatchingException { return Factory.iterator(installedIdentity, info); } }; } private static class IteratorState { protected PatchableTarget.TargetInfo currentInfo; protected int patchIndex; protected Patch.PatchType type = ONE_OFF; IteratorState(PatchableTarget.TargetInfo info) { this(info, -1); } IteratorState(PatchableTarget.TargetInfo info, int patchIndex) { if(info == null) { throw new IllegalArgumentException("Target info is null"); } this.currentInfo = info; this.patchIndex = patchIndex; } IteratorState(InstalledIdentity installedIdentity) throws PatchingException { if(installedIdentity == null) { throw new IllegalArgumentException("Installation manager is null."); } try { this.currentInfo = installedIdentity.getIdentity().loadTargetInfo(); } catch (IOException e) { throw new PatchingException(PatchLogger.ROOT_LOGGER.failedToLoadInfo(installedIdentity.getIdentity().getName())); } patchIndex = -1; } } private static final class IteratorImpl extends IteratorState implements Iterator { private final InstalledIdentity mgr; private IteratorImpl(InstalledIdentity mgr) throws PatchingException { super(mgr); this.mgr = mgr; } private IteratorImpl(TargetInfo info, InstalledIdentity mgr) { super(info); this.mgr = mgr; } @Override public boolean hasNext() { return hasNext(mgr, this); } private static boolean hasNext(InstalledIdentity installedIdentity, IteratorState state) { // current info hasn't been initialized yet if(state.patchIndex < 0) { if(BASE.equals(state.currentInfo.getCumulativePatchID())) { if(state.currentInfo.getPatchIDs().isEmpty()) { return false; // unpatched } } return true; } // check whether there are still one-offs left in the current info // one-offs + 1 means the cumulative patch has been returned as well final int size = state.currentInfo.getPatchIDs().size(); if(state.patchIndex < size) { return existsOnDisk(installedIdentity, state.currentInfo.getPatchIDs().get(state.patchIndex)); } // see whether there is the next CP final String releaseID = state.currentInfo.getCumulativePatchID(); if(BASE.equals(releaseID)) { return false; } // it's not the base yet and the cumulative has not been returned yet if(state.patchIndex == size) { return existsOnDisk(installedIdentity, state.currentInfo.getCumulativePatchID()); } // if it's not BASE then it's a specific patch, so it actually // means that there should more to iterate. But we rely on // the presence of the patch directory and its rollback.xml. File patchHistoryDir = installedIdentity.getInstalledImage().getPatchHistoryDir(releaseID); if(patchHistoryDir.exists()) { final File rollbackXml = new File(patchHistoryDir, "rollback.xml"); if(rollbackXml.exists()) { try { final PatchBuilder patchBuilder = (PatchBuilder)PatchXml.parse(rollbackXml); final RollbackPatch patch = (RollbackPatch) patchBuilder.build(); PatchableTarget.TargetInfo nextInfo = patch.getIdentityState().getIdentity().loadTargetInfo(); if(BASE.equals(nextInfo.getCumulativePatchID())) { if(nextInfo.getPatchIDs().isEmpty()) { return false; } } else if(!existsOnDisk(installedIdentity, nextInfo.getCumulativePatchID())) { return false; } } catch(Exception e) { throw new IllegalStateException(PatchLogger.ROOT_LOGGER.failedToLoadInfo(installedIdentity.getIdentity().getName()), e); } return true; } } return false; } @Override public Entry next() { return next(mgr, this); } private static Entry next(final InstalledIdentity installedIdentity, IteratorState state) { String patchId = nextPatchIdForCurrentInfo(state); if(patchId == null) { // current info is exhausted, try moving to the next CP if(state.patchIndex < 0) { state.patchIndex = 0; } else { final String releaseID = state.currentInfo.getCumulativePatchID(); if(BASE.equals(releaseID)) { throw new NoSuchElementException(PatchLogger.ROOT_LOGGER.noMorePatches()); } final File patchHistoryDir = installedIdentity.getInstalledImage().getPatchHistoryDir(releaseID); if(patchHistoryDir.exists()) { final File rollbackXml = new File(patchHistoryDir, "rollback.xml"); if(rollbackXml.exists()) { try { final PatchBuilder patchBuilder = (PatchBuilder)PatchXml.parse(rollbackXml); final RollbackPatch patch = (RollbackPatch) patchBuilder.build(); state.currentInfo = patch.getIdentityState().getIdentity().loadTargetInfo(); state.patchIndex = 0; state.type = ONE_OFF; } catch(Exception e) { throw new IllegalStateException(PatchLogger.ROOT_LOGGER.failedToLoadInfo(installedIdentity.getIdentity().getName()), e); } } else { throw new NoSuchElementException(PatchLogger.ROOT_LOGGER.patchIsMissingFile(rollbackXml.getAbsolutePath())); } } else { throw new NoSuchElementException(PatchLogger.ROOT_LOGGER.noPatchHistory(patchHistoryDir.getAbsolutePath())); } } patchId = nextPatchIdForCurrentInfo(state); if(patchId == null) { throw new NoSuchElementException(PatchLogger.ROOT_LOGGER.noMorePatches()); } assertExistsOnDisk(installedIdentity, patchId); } final String entryPatchId = patchId; final Patch.PatchType entryType = state.type; return new Entry() { String appliedAt; Map<String,String> layerPatches; Map<String,String> addOnPatches; Patch patch; @Override public String getPatchId() { return entryPatchId; } @Override public PatchType getType() { return entryType; } @Override public String getAppliedAt() { if(appliedAt == null) { final File patchHistoryDir = installedIdentity.getInstalledImage().getPatchHistoryDir(entryPatchId); if(patchHistoryDir.exists()) { try { appliedAt = getAppliedAt(patchHistoryDir); } catch (PatchingException e) { } } } return appliedAt; } @Override public Map<String, String> getLayerPatches() { if(layerPatches == null) { layerPatches = loadLayerPatches(false); } return layerPatches; } @Override public Map<String, String> getAddOnPatches() { if(addOnPatches == null) { addOnPatches = loadLayerPatches(true); } return addOnPatches; } private String getAppliedAt(File patchDir) throws PatchingException { File timestampFile = new File(patchDir, Constants.TIMESTAMP); try { return timestampFile.exists() ? PatchUtils.readRef(timestampFile) : null; } catch (IOException e) { throw new PatchingException(PatchLogger.ROOT_LOGGER.fileIsNotReadable(timestampFile.getAbsolutePath())); } } private Map<String,String> loadLayerPatches(boolean addons) { Map<String,String> result = Collections.emptyMap(); final Patch patch = getMetadata(); if(patch != null) { result = new HashMap<String, String>(); for (PatchElement e : patch.getElements()) { if (e.getProvider().isAddOn() == addons) { result.put(e.getProvider().getName(), e.getId()); } } } return result; } @Override public Patch getMetadata() { if(patch == null) { final File patchDir = installedIdentity.getInstalledImage().getPatchHistoryDir(entryPatchId); if(patchDir.exists()) { final File patchXml = new File(patchDir, "patch.xml"); if(patchXml.exists()) { try { patch = ((PatchBuilder)PatchXml.parse(patchXml)).build(); } catch (Exception e) { e.printStackTrace(); // TODO } } } } return patch; } }; } /** * @throws UnsupportedOperationException if the {@code remove} * operation is not supported by this iterator. */ @Override public void remove() { throw new UnsupportedOperationException(); } private static boolean existsOnDisk(InstalledIdentity mgr, String id) { try { assertExistsOnDisk(mgr, id); return true; } catch(NoSuchElementException e) { return false; } } private static void assertExistsOnDisk(InstalledIdentity mgr, String id) throws NoSuchElementException { final File historyDir = mgr.getInstalledImage().getPatchHistoryDir(id); if(!historyDir.exists()) { throw new NoSuchElementException(PatchLogger.ROOT_LOGGER.noPatchHistory(historyDir.getAbsolutePath())); } // TODO parsed xml can be cached final File rollbackXml = new File(historyDir, "rollback.xml"); if(!rollbackXml.exists()) { throw new NoSuchElementException(PatchLogger.ROOT_LOGGER.patchIsMissingFile(rollbackXml.getAbsolutePath())); } try { PatchXml.parse(rollbackXml); } catch (Exception e) { throw new NoSuchElementException(PatchLogger.ROOT_LOGGER.fileIsNotReadable(rollbackXml.getAbsolutePath() + ": " + e.getLocalizedMessage())); } final File patchXml = new File(historyDir, "patch.xml"); if(!patchXml.exists()) { throw new NoSuchElementException(PatchLogger.ROOT_LOGGER.patchIsMissingFile(patchXml.getAbsolutePath())); } try { PatchXml.parse(patchXml); } catch (Exception e) { throw new NoSuchElementException(PatchLogger.ROOT_LOGGER.fileIsNotReadable(patchXml.getAbsolutePath() + ": " + e.getLocalizedMessage())); } } /** * Returns the next patch id, be it a one-off or the CP * <b>for the current info</b>. If the current info has been * exhausted, the method returns null. */ private static String nextPatchIdForCurrentInfo(IteratorState state) { if(state.patchIndex < 0) { return null; } final int size = state.currentInfo.getPatchIDs().size(); if(state.patchIndex < size) { return state.currentInfo.getPatchIDs().get(state.patchIndex++); } else if(state.patchIndex == size) { ++state.patchIndex; state.type = CUMULATIVE; final String cp = state.currentInfo.getCumulativePatchID(); return BASE.equals(cp) ? null : cp; } return null; } @Override public boolean hasNextCP() { final IteratorState state = new IteratorState(currentInfo, patchIndex); return nextCP(mgr, state) != null; } @Override public Entry nextCP() { final IteratorState state = new IteratorState(currentInfo, patchIndex); final Entry entry = nextCP(mgr, state); if(entry == null) { throw new NoSuchElementException(PatchLogger.ROOT_LOGGER.noMorePatches()); } currentInfo = state.currentInfo; patchIndex = state.patchIndex; type = state.type; return entry; } private static Entry nextCP(InstalledIdentity mgr, IteratorState state) { while(hasNext(mgr, state)) { final Entry entry = next(mgr, state); if(state.type == Patch.PatchType.CUMULATIVE) { return entry; } } return null; } } /** * Goes back in rollback history adding the patch id and it's application timestamp * to the resulting list. */ private static void fillHistoryIn(InstalledIdentity installedImage, PatchableTarget.TargetInfo info, ModelNode result) throws PatchingException { final Iterator i = iterator(installedImage, info); while(i.hasNext()) { final Entry next = i.next(); fillHistoryIn(result, next.getType(), next.getPatchId(), next.getAppliedAt()); } } private static void fillHistoryIn(ModelNode result, PatchType type, String patchID, String appliedAt) throws PatchingException { ModelNode history = new ModelNode(); history.get(Constants.PATCH_ID).set(patchID); history.get(Constants.TYPE).set(type.getName()); final ModelNode appliedAtNode = history.get(Constants.APPLIED_AT); if(appliedAt != null) { appliedAtNode.set(appliedAt); } result.add(history); } } }