code: /**
   * Copyright 2018 Alexey Ragozin
   *
   * Licensed 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.gridkit.jvmtool.hflame;
  
  import static org.gridkit.jvmtool.hflame.XmlUtil.elementsOf;
  import static org.gridkit.jvmtool.hflame.XmlUtil.href;
  import static org.gridkit.jvmtool.hflame.XmlUtil.id;
  import static org.gridkit.jvmtool.hflame.XmlUtil.isScript;
  import static org.gridkit.jvmtool.hflame.XmlUtil.isStyleSheet;
  import static org.gridkit.jvmtool.hflame.XmlUtil.textOf;
  
  import java.io.BufferedReader;
  import java.io.IOException;
  import java.io.InputStream;
  import java.io.InputStreamReader;
  import java.io.Writer;
  import java.nio.charset.Charset;
  import java.util.HashMap;
  import java.util.Map;
  
  import javax.xml.transform.OutputKeys;
  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 org.w3c.dom.Document;
  import org.w3c.dom.Element;
  import org.w3c.dom.Text;
  
  public class FlameTemplateProcessor {
  
      private final Document template;
      private final Map<String, String> imports = new HashMap<String, String>();
      private final Map<String, JsonFlameDataSet> datasets = new HashMap<String, JsonFlameDataSet>();
      private boolean retainDebug = false;
  
      public FlameTemplateProcessor(Document template) {
          this.template = template;
          initDefaultScripts();
      }
  
      private void initDefaultScripts() {
          // nothing
      }
  
      public void retainDebug(boolean retain) {
          retainDebug = retain;
      }
  
      public void setDataSet(String name, JsonFlameDataSet dataSet) {
          datasets.put(name, dataSet);
      }
  
      public void generate(Writer output) throws IOException {
          Document doc = (Document) template.cloneNode(true);
          transformHead((Element) doc.getDocumentElement().getElementsByTagName("head").item(0));
          transformBody((Element) doc.getDocumentElement().getElementsByTagName("body").item(0));
          encodeDocument(doc, output);
      }
  
      private void encodeDocument(Document doc, Writer output) {
          try {
  
              Transformer transformer = TransformerFactory.newInstance().newTransformer();
              transformer.setOutputProperty(OutputKeys.ENCODING, "utf8");
  
              StreamResult result = new StreamResult(output);
              DOMSource source = new DOMSource(doc.getDocumentElement());
              transformer.transform(source, result);
  
          } catch (TransformerConfigurationException e) {
              throw new RuntimeException(e);
          } catch (TransformerFactoryConfigurationError e) {
              throw new RuntimeException(e);
          } catch (TransformerException e) {
              throw new RuntimeException(e);
          }
      }
  
      private void transformHead(Element head) throws IOException {
          for(Element e: elementsOf(head)) {
              String id = id(e);
              String href = href(e);
              if (isStyleSheet(e) || isScript(e)) {
                  if (id == null) {
                      if (href != null && !href.startsWith("..")) {
                          if (isStyleSheet(e)) {
                              // inline stylesheet
                              Element re = head.getOwnerDocument().createElement("style");
                              head.replaceChild(re, e);
                              importCss(href, re);
                          }
                          else {
                              // inline script
                              e.removeAttribute("src");
                              importJs(href, e);
                          }
                      }
                      else {
                          // remove elements with ../ source
                          head.removeChild(e);
                      }
                  }
                  else {
                      // keep "identified" elements as is
                  }
              }
              if (id != null && id.startsWith("debug_")) {
                  if (!retainDebug) {
                      head.removeChild(e);
                  }
              }
              else if (id != null && id.startsWith("importflame_")) {
                  String flameName = id.substring("importflame_".length());
                  e.removeAttribute("id");
                  e.removeAttribute("src");
                  importDataSet(flameName, e);
              }
          }
      }
  
      private void transformBody(Element node) throws IOException {
          for(Element e: elementsOf(node)) {
              String id = id(e);
              if (id != null && id.startsWith("debug_")) {
                  if (!retainDebug) {
                      node.removeChild(e);
                  }
              }
              else if (id != null && id.startsWith("importflame_")) {
                  String flameName = id.substring("importflame_".length());
                  e.removeAttribute("id");
                  e.removeAttribute("src");
                  importDataSet(flameName, e);
              }
              else {
                  transformBody(e);
              }
          }
      }
  
      private InputStream openResource(String res) throws IOException {
          return Thread.currentThread().getContextClassLoader().getResourceAsStream(res);
      }
  
      private void importCss(String cssName, Element e) throws IOException {
          InputStream is = openResource(cssName);
          if (is == null) {
              String res = imports.get("css:" + cssName);
              if (res != null) {
                  is = openResource(res);
              }
          }
          if (is == null) {
              throw new IllegalArgumentException("Unknown CSS resource: " + cssName);
          }
          loadContent(e, is);
      }
  
      private void importJs(String jsName, Element e) throws IOException {
          InputStream is = openResource(jsName);
          if (is == null) {
              String res = imports.get("js:" + jsName);
              if (res != null) {
                  is = openResource(res);
              }
          }
          if (is == null) {
              throw new IllegalArgumentException("Unknown script name: " + jsName);
          }
          loadContent(e, is);
      }
  
      private void importDataSet(String flameName, Element e) {
          JsonFlameDataSet dataSet = datasets.get(flameName);
          if (dataSet == null) {
              throw new IllegalArgumentException("Unknown data set name: " + flameName);
          }
  
          for(Text t: textOf(e)) {
              e.removeChild(t);
          }
  
          StringBuilder sb = new StringBuilder();
          sb.append("$(document).ready(function() {createFlameChart(\"" + flameName + "\", ");
          dataSet.exportJson(sb);
          sb.append(").initFlameChart()});\n");
          Text text = e.getOwnerDocument().createCDATASection(sb.toString());
          e.appendChild(text);
      }
  
      private void loadContent(Element e, InputStream is) throws IOException {
          BufferedReader reader = new BufferedReader(new InputStreamReader(is, Charset.forName("UTF8")));
          StringBuilder sb = new StringBuilder();
          while(true) {
              String line = reader.readLine();
              if (line == null) {
                  break;
              }
              if (line.contains("!!THROW AWAY BELOW!!")) {
                  break;
              }
              if (!retainDebug && line.contains("debug(") && line.contains(");")) {
                  // filtering out debug
                  continue;
              }
              for(int i = 0; i != line.length(); ++i) {
                  char ch = line.charAt(i);
                  if (ch != '\r' ) {
                      sb.append(ch);
                  }
              }
              sb.append('\n');
          }
          reader.close();
  
          for(Text t: textOf(e)) {
              e.removeChild(t);
          }
  
          Text text = e.getOwnerDocument().createCDATASection(sb.toString());
          e.appendChild(text);
      }
  }
  