001/*
002 * (C) Copyright 2006-2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     bstefanescu, jcarsique
018 */
019package org.nuxeo.connect.update.task.standalone;
020
021import static java.nio.charset.StandardCharsets.UTF_8;
022
023import java.io.File;
024import java.io.IOException;
025import java.io.Reader;
026import java.io.StringReader;
027import java.util.ArrayList;
028import java.util.LinkedList;
029import java.util.List;
030import java.util.Map;
031
032import javax.xml.parsers.DocumentBuilder;
033import javax.xml.parsers.DocumentBuilderFactory;
034import javax.xml.parsers.ParserConfigurationException;
035
036import org.apache.commons.io.FileUtils;
037import org.nuxeo.connect.update.LocalPackage;
038import org.nuxeo.connect.update.PackageException;
039import org.nuxeo.connect.update.PackageUpdateService;
040import org.nuxeo.connect.update.ValidationStatus;
041import org.nuxeo.connect.update.task.Command;
042import org.nuxeo.connect.update.xml.XmlWriter;
043import org.w3c.dom.Document;
044import org.w3c.dom.Element;
045import org.w3c.dom.Node;
046import org.xml.sax.InputSource;
047import org.xml.sax.SAXException;
048
049/**
050 * A command based task.
051 *
052 * @author <a href="mailto:bs@nuxeo.com">Bogdan Stefanescu</a>
053 */
054public abstract class CommandsTask extends AbstractTask {
055
056    protected final List<Command> commands;
057
058    /**
059     * The log is generated in the inverse order of commands to ensure last command is rollbacked first.
060     */
061    protected final LinkedList<Command> commandLog;
062
063    public CommandsTask(PackageUpdateService pus) {
064        super(pus);
065        commands = new ArrayList<>();
066        commandLog = new LinkedList<>();
067    }
068
069    /**
070     * Get the commands file from where to load commands for this task.
071     */
072    protected abstract File getCommandsFile() throws PackageException;
073
074    @SuppressWarnings("hiding")
075    @Override
076    public void initialize(LocalPackage pkg, boolean restart) throws PackageException {
077        super.initialize(pkg, restart);
078        loadCommands();
079    }
080
081    /**
082     * Load the commands of this task given the user parameters. The parameter map may be null.
083     */
084    protected void loadCommands() throws PackageException {
085        try {
086            String content = loadParametrizedFile(getCommandsFile(), env);
087            StringReader reader = new StringReader(content);
088            readLog(reader);
089        } catch (IOException e) {
090            throw new PackageException("Failed to load commands file", e);
091        }
092    }
093
094    /**
095     * Gets the commands to execute.
096     */
097    public List<Command> getCommands() {
098        return commands;
099    }
100
101    /**
102     * Gets the command log. These are the commands ran so far.
103     */
104    public List<Command> getCommandLog() {
105        return commandLog;
106    }
107
108    /**
109     * Adds a command to this task.
110     */
111    public void addCommand(Command command) {
112        commands.add(command);
113    }
114
115    /**
116     * User parameters are not handled by default. You need to implement your own task to do this.
117     */
118    @Override
119    protected void doRun(Map<String, String> params) throws PackageException {
120        for (Command cmd : commands) {
121            Command rollbackCmd = cmd.run(this, params);
122            if (rollbackCmd != null) {
123                if (rollbackCmd.isPostInstall()) {
124                    commandLog.add(rollbackCmd);
125                } else {
126                    commandLog.addFirst(rollbackCmd);
127                }
128            }
129        }
130        // XXX: force a flush?
131        flush();
132    }
133
134    /**
135     * @throws PackageException
136     * @since 5.6
137     */
138    protected abstract void flush() throws PackageException;
139
140    @Override
141    protected void doRollback() throws PackageException {
142        while (!commandLog.isEmpty()) {
143            commandLog.removeFirst().run(this, null);
144        }
145    }
146
147    @Override
148    public void doValidate(ValidationStatus status) throws PackageException {
149        // the target platform is not checked at install
150        // check that commands can be run
151        for (Command cmd : commands) {
152            cmd.validate(this, status);
153        }
154    }
155
156    public void writeLog(File file) throws PackageException {
157        XmlWriter writer = new XmlWriter();
158        writer.start("uninstall");
159        writer.startContent();
160        for (Command cmd : commandLog) {
161            cmd.writeTo(writer);
162        }
163        writer.end("uninstall");
164        try {
165            // replace all occurrences of the installation path with the corresponding variable otherwise the uninstall
166            // will not work after renaming the installation directory
167            String content = parametrizePaths(writer.toString());
168            // replace '//' by '/' if any
169            content = content.replace(File.separator.concat(File.separator), File.separator);
170            FileUtils.writeStringToFile(file, content, UTF_8);
171        } catch (IOException e) {
172            throw new PackageException("Failed to write commands", e);
173        }
174    }
175
176    public String parametrizePaths(String content) {
177        return content.replace(serverPathPrefix, "${" + ENV_SERVER_HOME + "}/");
178    }
179
180    public void readLog(Reader reader) throws PackageException {
181        try {
182            DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
183            factory.setNamespaceAware(true);
184            DocumentBuilder builder = factory.newDocumentBuilder();
185            Document document = builder.parse(new InputSource(reader));
186            Element root = document.getDocumentElement();
187            Node node = root.getFirstChild();
188            while (node != null) {
189                if (node.getNodeType() == Node.ELEMENT_NODE) {
190                    Element element = (Element) node;
191                    String id = node.getNodeName();
192                    Command cmd = service.getCommand(id);
193                    if (cmd == null) { // may be the name of an embedded class
194                        try {
195                            cmd = (Command) pkg.getData().loadClass(id).getConstructor().newInstance();
196                        } catch (ReflectiveOperationException t) {
197                            throw new PackageException("Unknown command: " + id);
198                        }
199                    }
200                    cmd.initialize(element);
201                    cmd.setPackageUpdateService(service);
202                    commands.add(cmd);
203                }
204                node = node.getNextSibling();
205            }
206        } catch (ParserConfigurationException | SAXException | IOException e) {
207            throw new PackageException("Failed to read commands", e);
208        }
209    }
210
211}