/******************************************************************************* * Copyright (c) 2011-2013 EclipseSource Muenchen GmbH and others. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Johannes Faltermeier - initial API and implementation ******************************************************************************/ package org.eclipse.emf.emfstore.internal.server.startup; import java.io.File; import java.io.FilenameFilter; import java.io.IOException; import java.lang.reflect.InvocationTargetException; import java.text.MessageFormat; import java.util.ArrayList; import java.util.List; import javax.xml.parsers.DocumentBuilder; import javax.xml.parsers.DocumentBuilderFactory; import javax.xml.parsers.ParserConfigurationException; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerConfigurationException; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.TransformerFactoryConfigurationError; import javax.xml.transform.dom.DOMSource; import javax.xml.transform.stream.StreamResult; import javax.xml.xpath.XPath; import javax.xml.xpath.XPathConstants; import javax.xml.xpath.XPathExpression; import javax.xml.xpath.XPathExpressionException; import javax.xml.xpath.XPathFactory; import org.eclipse.emf.emfstore.internal.common.model.util.FileUtil; import org.eclipse.emf.emfstore.internal.common.model.util.ModelUtil; import org.eclipse.emf.emfstore.internal.server.ServerConfiguration; import org.w3c.dom.DOMException; import org.w3c.dom.Document; import org.w3c.dom.NamedNodeMap; import org.w3c.dom.Node; import org.w3c.dom.NodeList; import org.xml.sax.SAXException; /** * When using the default EMF XMI persistence HRefs between resources are persisted based on the non-normalized URIs of * the resources. Since version 1.1 introduced a new URI scheme for EMFStore, files that were persisted with version 1.0 * and prior need to be migrated. * <p> * This migrator will update the HRefs in legacy files on the server side. * * @author jfaltermeier * */ public class ServerHrefMigrator { private static final String STORAGE_USS = "storage.uss"; //$NON-NLS-1$ private static final String PROJECT_PREFIX = "project-"; //$NON-NLS-1$ private File backup; private List<String> corruptProjectIds = new ArrayList<String>(); /** * Performs the migration, if needed. Creates a backup beforehand. * * @return <code>true</code> if migration was successful, <code>false</code> if an error occurred and the server * startup should be canceled. */ public boolean migrate() { final String serverHome = ServerConfiguration.getServerHome(); // check if migration is needed if (isMigrationNeeded(serverHome + STORAGE_USS)) { if (backup != null) { return false; } try { backup = createBackup(ServerConfiguration.getServerHome(), ServerConfiguration.getServerHome() + "../backup" + System.currentTimeMillis()); //$NON-NLS-1$ } catch (final IOException ex) { ModelUtil.logException( Messages.ServerHrefMigrator_ErrorDuringBackup, ex); return false; } // perform migration try { corruptProjectIds = doMigrate(serverHome); return true; } catch (final InvocationTargetException ex) { ModelUtil.logException( Messages.ServerHrefMigrator_ErrorDuringMigration, ex); return false; } } return true; } private boolean isMigrationNeeded(String pathToServerSpace) { try { final String toMatch = getProjectAttribute(pathToServerSpace); if (toMatch == null) { return false; } return toMatch.contains("projectHistory.uph"); //$NON-NLS-1$ } catch (final ParserConfigurationException ex) { ModelUtil.logException( Messages.ServerHrefMigrator_SkipMigration, ex); } catch (final SAXException ex) { ModelUtil.logException( Messages.ServerHrefMigrator_SkipMigration, ex); } catch (final IOException ex) { ModelUtil.logException( Messages.ServerHrefMigrator_SkipMigration, ex); } try { backup = createBackup(ServerConfiguration.getServerHome(), ServerConfiguration.getServerHome() + "../backup" + System.currentTimeMillis()); //$NON-NLS-1$ } catch (final IOException ex) { backup = new File(""); //$NON-NLS-1$ ModelUtil.logException( Messages.ServerHrefMigrator_BackupFailed, ex); } return true; } /** * Creates a backup. * * @param from path to source * @param to path to destination * @return the backup file * @throws IOException on a IO problem during backup creation */ protected File createBackup(String from, String to) throws IOException { final File sourceFile = new File(from); final File backupFile = new File(to); FileUtil.copyDirectory(sourceFile, backupFile); return backupFile; } private List<String> doMigrate(String serverHome) throws InvocationTargetException { migrateNonContainment(serverHome + STORAGE_USS, "projects", new ServerSpaceRule()); //$NON-NLS-1$ final File serverHomeFile = new File(serverHome); final File[] projectFiles = serverHomeFile.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return name.startsWith(PROJECT_PREFIX); } }); final List<String> corruptProjectIds = new ArrayList<String>(); for (final File f : projectFiles) { try { final String projectHistoryPath = f.getAbsolutePath() + "/projectHistory.uph"; //$NON-NLS-1$ if (!new File(projectHistoryPath).exists()) { ModelUtil.logWarning(MessageFormat.format( Messages.ServerHrefMigrator_HistoryFileDoeNotExists, projectHistoryPath)); removeReferencesToCorruptProject(serverHome + STORAGE_USS, f.getName().substring(PROJECT_PREFIX.length())); continue; } migrateContainmentHRefs(projectHistoryPath, "versions", //$NON-NLS-1$ new VersionRule()); final File[] versions = f.listFiles(new FilenameFilter() { public boolean accept(File dir, String name) { return name.startsWith("version-"); //$NON-NLS-1$ } }); for (final File version : versions) { final String versionPath = version.getAbsolutePath(); migrateNonContainment(versionPath, "nextVersion", new VersionMultiRule()); //$NON-NLS-1$ migrateNonContainment(versionPath, "previousVersion", new VersionMultiRule()); //$NON-NLS-1$ migrateNonContainment(versionPath, "ancestorVersion", new VersionMultiRule()); //$NON-NLS-1$ migrateNonContainment(versionPath, "branchedVersions", new VersionMultiRule()); //$NON-NLS-1$ migrateNonContainment(versionPath, "mergedToVersion", new VersionMultiRule()); //$NON-NLS-1$ migrateNonContainment(versionPath, "mergedFromVersion", new VersionMultiRule()); //$NON-NLS-1$ } } catch (final InvocationTargetException exception) { ModelUtil.logException(MessageFormat.format( Messages.ServerHrefMigrator_MigrationFailed, f.getAbsolutePath()), exception); corruptProjectIds.add(f.getName().substring(PROJECT_PREFIX.length())); continue; } } return corruptProjectIds; } private String getProjectAttribute(String pathToFile) throws ParserConfigurationException, SAXException, IOException { final DocumentBuilderFactory docFactory = DocumentBuilderFactory .newInstance(); final DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); final Document doc = docBuilder.parse(pathToFile); final Node serverSpace = doc.getFirstChild(); final NamedNodeMap attr = serverSpace.getAttributes(); final Node nodeAttr = attr.getNamedItem("projects"); //$NON-NLS-1$ if (nodeAttr == null) { return null; } final String projectsOld = nodeAttr.getTextContent(); final String[] projects = projectsOld.split(" "); //$NON-NLS-1$ if (projects.length < 1) { return null; } return projects[0]; } /** * Updates the href attribute in tags with the given name. * * @param pathToFile the path of the xmi file to be updated * @param tagName the tag for which the hrefs are to be updated * @param rule the rule computing the new value * @throws InvocationTargetException in case of error */ protected void migrateContainmentHRefs(String pathToFile, String tagName, UpdateXMIAttributeRule rule) throws InvocationTargetException { try { final DocumentBuilderFactory docFactory = DocumentBuilderFactory .newInstance(); final DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); final Document doc = docBuilder.parse(pathToFile); final NodeList tagElements = doc.getElementsByTagName(tagName); for (int i = 0; i < tagElements.getLength(); i++) { final Node pS = tagElements.item(i); final NamedNodeMap attr = pS.getAttributes(); final Node nodeAttr = attr.getNamedItem("href"); //$NON-NLS-1$ final String hrefOld = nodeAttr.getTextContent(); final String hrefNew = rule.getNewAttribute(hrefOld); nodeAttr.setTextContent(hrefNew); } final TransformerFactory transformerFactory = TransformerFactory .newInstance(); final Transformer transformer = transformerFactory.newTransformer(); final DOMSource source = new DOMSource(doc); final StreamResult result = new StreamResult(new File(pathToFile)); transformer.transform(source, result); } catch (final DOMException ex) { throw new InvocationTargetException(ex); } catch (final TransformerConfigurationException ex) { throw new InvocationTargetException(ex); } catch (final ParserConfigurationException ex) { throw new InvocationTargetException(ex); } catch (final SAXException ex) { throw new InvocationTargetException(ex); } catch (final IOException ex) { throw new InvocationTargetException(ex); } catch (final TransformerFactoryConfigurationError ex) { throw new InvocationTargetException(ex); } catch (final TransformerException ex) { throw new InvocationTargetException(ex); } } private void removeReferencesToCorruptProject(String serverHome, String projectId) throws InvocationTargetException { try { final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); final DocumentBuilder builder = factory.newDocumentBuilder(); final Document doc = builder.parse(serverHome); final XPathFactory xPathfactory = XPathFactory.newInstance(); final XPath xpath = xPathfactory.newXPath(); final XPathExpression expr = xpath.compile("//projects[@id=\"" + projectId + "\"]"); //$NON-NLS-1$ //$NON-NLS-2$ final NodeList nl = (NodeList) expr.evaluate(doc, XPathConstants.NODESET); for (int i = 0; i < nl.getLength(); i++) { nl.item(i).getParentNode().removeChild(nl.item(i)); } final TransformerFactory transformerFactory = TransformerFactory .newInstance(); final Transformer transformer = transformerFactory.newTransformer(); final DOMSource source = new DOMSource(doc); final StreamResult result = new StreamResult(new File(serverHome)); transformer.transform(source, result); } catch (final ParserConfigurationException ex) { throw new InvocationTargetException(ex); } catch (final SAXException ex) { throw new InvocationTargetException(ex); } catch (final IOException ex) { throw new InvocationTargetException(ex); } catch (final XPathExpressionException ex) { throw new InvocationTargetException(ex); } catch (final TransformerConfigurationException ex) { throw new InvocationTargetException(ex); } catch (final TransformerException ex) { throw new InvocationTargetException(ex); } } /** * Updates the attribute with the given name. * * @param pathToFile the path of the xmi file to be updated * @param tagName the tag for which the attribute contents are to be updated * @param rule the rule computing the new value * @throws InvocationTargetException in case of error */ protected void migrateNonContainment(String pathToFile, String tagName, UpdateXMIAttributeRule rule) throws InvocationTargetException { try { final DocumentBuilderFactory docFactory = DocumentBuilderFactory .newInstance(); final DocumentBuilder docBuilder = docFactory.newDocumentBuilder(); final Document doc = docBuilder.parse(pathToFile); final Node serverSpace = doc.getFirstChild(); final NamedNodeMap attr = serverSpace.getAttributes(); final Node nodeAttr = attr.getNamedItem(tagName); if (nodeAttr == null) { return; } final String attributeOld = nodeAttr.getTextContent(); final String attributeNew = rule.getNewAttribute(attributeOld); nodeAttr.setTextContent(attributeNew); final TransformerFactory transformerFactory = TransformerFactory .newInstance(); final Transformer transformer = transformerFactory.newTransformer(); final DOMSource source = new DOMSource(doc); final StreamResult result = new StreamResult(new File(pathToFile)); transformer.transform(source, result); } catch (final DOMException ex) { throw new InvocationTargetException(ex); } catch (final TransformerConfigurationException ex) { throw new InvocationTargetException(ex); } catch (final ParserConfigurationException ex) { throw new InvocationTargetException(ex); } catch (final SAXException ex) { throw new InvocationTargetException(ex); } catch (final IOException ex) { throw new InvocationTargetException(ex); } catch (final TransformerFactoryConfigurationError ex) { throw new InvocationTargetException(ex); } catch (final TransformerException ex) { throw new InvocationTargetException(ex); } } /** * Returns a list of project IDs that could not be migrated. * * @return the corruptProjectIds */ public List<String> getCorruptProjectIds() { return corruptProjectIds; } }