001/**
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *      http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018
019package org.apache.oozie.util.graph;
020
021import com.google.common.base.Preconditions;
022import com.google.common.base.Strings;
023import org.apache.oozie.client.WorkflowJob;
024import org.xml.sax.InputSource;
025import org.xml.sax.SAXException;
026import org.xml.sax.XMLReader;
027
028import javax.xml.XMLConstants;
029import javax.xml.parsers.ParserConfigurationException;
030import javax.xml.parsers.SAXParser;
031import javax.xml.parsers.SAXParserFactory;
032import java.io.IOException;
033import java.io.OutputStream;
034import java.io.StringReader;
035
036/**
037 * Class to generate and plot runtime workflow DAG.
038 * <p>
039 * Since it delegates to {@link WorkflowGraphHandler} and a {@link GraphRenderer}, it is the single entry point when changing graph
040 * generation behavior.
041 */
042public class GraphGenerator {
043    public static final String SAX_FEATURE_EXTERNAL_GENERAL_ENTITIES = "http://xml.org/sax/features/external-general-entities";
044    public static final String SAX_FEATURE_EXTERNAL_PARAMETER_ENTITIES = "http://xml.org/sax/features/external-parameter-entities";
045    public static final String SAX_FEATURE_DISALLOW_DOCTYPE_DECL = "http://apache.org/xml/features/disallow-doctype-decl";
046    private final GraphRenderer graphRenderer;
047    private final String xml;
048    private final WorkflowJob job;
049    private final boolean showKill;
050
051    /**
052     * Constructor Inversion of Control-style for better testability.
053     * @param xml The workflow definition XML
054     * @param job Current status of the job
055     * @param showKill Flag to whether show 'kill' node
056     * @param graphRenderer the renderer
057     */
058    public GraphGenerator(final String xml, final WorkflowJob job, final boolean showKill, final GraphRenderer graphRenderer) {
059        Preconditions.checkArgument(!Strings.isNullOrEmpty(xml), "xml can't be empty");
060        Preconditions.checkArgument(job != null, "WorkflowJob can't be null");
061
062        this.xml = xml;
063        this.job = job;
064        this.showKill = showKill;
065        this.graphRenderer = graphRenderer;
066    }
067
068    /**
069     * Stream the generated PNG, DOT or SVG stream to the caller. Note that closing the {@link OutputStream} is the responsibility
070     * of the caller.
071     *
072     * @param out the {@link OutputStream} to use on streaming
073     * @param outputFormat The output format to apply when rendering
074     * @throws ParserConfigurationException if the parser is not configured properly
075     * @throws SAXException in case of XML error
076     * @throws IOException in case of any other IO related issues
077     */
078    public void write(final OutputStream out, final OutputFormat outputFormat)
079            throws ParserConfigurationException, SAXException, IOException {
080        final XMLReader xmlReader = newXmlReader();
081        xmlReader.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
082
083        xmlReader.setContentHandler(
084                new WorkflowGraphHandler(out, outputFormat, job, showKill, graphRenderer));
085
086        try (final StringReader stringReader = new StringReader(xml)) {
087            xmlReader.parse(new InputSource(stringReader));
088        }
089    }
090
091    private XMLReader newXmlReader() throws ParserConfigurationException, SAXException {
092        final SAXParserFactory spf = SAXParserFactory.newInstance();
093        spf.setFeature(SAX_FEATURE_EXTERNAL_GENERAL_ENTITIES, false);
094        spf.setFeature(SAX_FEATURE_EXTERNAL_PARAMETER_ENTITIES, false);
095        spf.setFeature(SAX_FEATURE_DISALLOW_DOCTYPE_DECL, true);
096        spf.setNamespaceAware(true);
097
098        final SAXParser saxParser = spf.newSAXParser();
099        final XMLReader xmlReader = saxParser.getXMLReader();
100        xmlReader.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
101
102        return xmlReader;
103    }
104}