All Downloads are FREE. Search and download functionalities are using the official Maven repository.

net.jangaroo.exml.compiler.ExmlToMxml Maven / Gradle / Ivy

There is a newer version: 4.1.0-alpha-8
Show newest version
package net.jangaroo.exml.compiler;

import net.jangaroo.exml.api.ExmlcException;
import net.jangaroo.exml.generator.MxmlLibraryManifestGenerator;
import net.jangaroo.exml.model.ConfigClass;
import net.jangaroo.exml.model.ConfigClassRegistry;
import net.jangaroo.exml.model.ConfigClassType;
import net.jangaroo.exml.model.Declaration;
import net.jangaroo.exml.model.ExmlModel;
import net.jangaroo.exml.model.ExmlSourceFile;
import net.jangaroo.exml.model.PublicApiMode;
import net.jangaroo.exml.parser.ExmlToConfigClassParser;
import net.jangaroo.exml.parser.ExmlToModelParser;
import net.jangaroo.exml.utils.ExmlUtils;
import net.jangaroo.jooc.Jooc;
import net.jangaroo.jooc.ast.AstNode;
import net.jangaroo.jooc.ast.CompilationUnit;
import net.jangaroo.jooc.ast.ImportDirective;
import net.jangaroo.jooc.backend.ApiModelGenerator;
import net.jangaroo.jooc.json.JsonObject;
import net.jangaroo.jooc.model.CompilationUnitModel;
import net.jangaroo.jooc.model.MethodModel;
import net.jangaroo.jooc.model.ParamModel;
import net.jangaroo.jooc.mxml.CatalogComponentsParser;
import net.jangaroo.jooc.mxml.MxmlComponentRegistry;
import net.jangaroo.jooc.mxml.MxmlUtils;
import net.jangaroo.utils.AS3Type;
import net.jangaroo.utils.CharacterRecordingHandler;
import net.jangaroo.utils.CompilerUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.xml.sax.Attributes;
import org.xml.sax.Locator;
import org.xml.sax.SAXException;
import org.xml.sax.ext.LexicalHandler;
import org.xml.sax.helpers.AttributesImpl;

import javax.xml.namespace.QName;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;

/**
 * A tool that converts EXML source code into MXML and ActionScript source code.
 */
public class ExmlToMxml {
  private static final String LOCAL_NAMESPACE = "local";

  private ConfigClassRegistry configClassRegistry;
  private Properties migrationMap = new Properties();
  private ClassLoader resourceClassLoader = ExmlToMxml.class.getClassLoader();
  private MxmlComponentRegistry mxmlComponentRegistry = new MxmlComponentRegistry();

  public ExmlToMxml(ConfigClassRegistry configClassRegistry) {
    this.configClassRegistry = configClassRegistry;

    if (configClassRegistry.getConfig().getExtAsJar() != null) {
      try {
        resourceClassLoader = new URLClassLoader(new URL[]{
                new URL("jar:" + configClassRegistry.getConfig().getExtAsJar().toURI().toString() + "!/")
        });
      } catch (IOException e) {
        throw new ExmlcException("Unable to configure resource class loader", e);
      }
    }
  }

  public File[] convert() {
    try {
      this.migrationMap.load(resourceClassLoader.getResourceAsStream("ext-as-3.4-migration-map.properties"));
    } catch (IOException e) {
      throw new ExmlcException("Unable to load migration map", e);
    }

    new CatalogComponentsParser(mxmlComponentRegistry).parse(resourceClassLoader.getResourceAsStream("catalog.xml"));

    try {
      new MxmlLibraryManifestGenerator(configClassRegistry).createManifestFile();
    } catch (IOException e) {
      throw new ExmlcException("Unable to create manifest.xml file: " + e.getMessage(), e);
    }

    Map exmlSourceFilesByConfigClassName = configClassRegistry.getExmlSourceFilesByConfigClassName();
    List mxmlFiles = new ArrayList();
    Collection exmlSourceFiles;
    List sourceFiles = configClassRegistry.getConfig().getSourceFiles();
    if (sourceFiles.isEmpty()) {
      exmlSourceFiles = exmlSourceFilesByConfigClassName.values();
    } else {
      exmlSourceFiles = new ArrayList();
      for (File exmlFile : sourceFiles) {
        exmlSourceFiles.add(configClassRegistry.getExmlSourceFile(exmlFile));
      }
    }
    for (ExmlSourceFile exmlSourceFile : exmlSourceFiles) {
      System.out.printf("Converting EXML file %s...%n", exmlSourceFile.getSourceFile());
      if (exmlSourceFile.hasSourceTargetClass()) {
        System.out.printf("  Target class %s is implemented in ActionScript: no need to generate MXML target class.", exmlSourceFile.getTargetClassName());
      } else {
        try {
          File mxmlFile = exmlToMxml(exmlSourceFile);
          mxmlFiles.add(mxmlFile);
          System.out.printf("  Generated MXML target class %s into file %s.%n", exmlSourceFile.getTargetClassName(), mxmlFile.getPath());
        } catch (Exception e) {
          throw new ExmlcException("Unable to convert to MXML: " + e.getMessage(), exmlSourceFile.getSourceFile(), e);
        }
      }
    }
    if (!configClassRegistry.getConfig().isKeepExmlFiles()) {
      // clean up EXML files:
      for (ExmlSourceFile exmlSourceFile : exmlSourceFiles) {
        if (!exmlSourceFile.getSourceFile().delete()) {
          System.err.println("Failed to delete EXML source file " + exmlSourceFile.getSourceFile().getPath());
        }
      }
    }
    return mxmlFiles.toArray(new File[mxmlFiles.size()]);
  }

  private File exmlToMxml(ExmlSourceFile exmlSourceFile) throws IOException, SAXException {
    ExmlModel exmlModel = new ExmlToModelParser(configClassRegistry).parse(exmlSourceFile.getSourceFile());
    File sourceFile = exmlSourceFile.getSourceFile();
    File outputFile = CompilerUtils.fileFromQName(exmlSourceFile.getTargetClassName(), configClassRegistry.getConfig().getSourcePath().get(0), ".mxml");
    FileUtils.forceMkdir(outputFile.getParentFile());
    PrintStream writer = new PrintStream(new FileOutputStream(outputFile), true, net.jangaroo.exml.api.Exmlc.OUTPUT_CHARSET);
    ExmlToConfigClassParser.parseFileWithHandler(sourceFile, new ExmlToMxmlHandler(exmlSourceFile, exmlModel, writer));
    return outputFile;
  }

  private class ExmlToMxmlHandler extends CharacterRecordingHandler implements LexicalHandler {
    private static final String OPEN_FX_SCRIPT = "%n  ";
    private static final String PUBLIC_STATIC_CONST_DECLARATION_FORMAT = "    public static const %s:%s = %s;%n%n";

    private final PrintStream out;
    private PrintStream currentOut;
    private boolean insideCdata;
    private boolean insideExmlObject;
    private ByteArrayOutputStream elementRecorder;
    private int lastColumn = 1;
    private Locator locator;
    private Map prefixMappings = new LinkedHashMap();
    private List metaData = new ArrayList();
    private boolean inMetaData;
    private boolean inConfigDescription;
    private boolean inConfigDefault;
    private String originalRootName;
    private String originalRootUri;
    private String exmlPrefix;
    private final Map configDefaultSubElements = new LinkedHashMap();
    private final Map configDefaultTypes = new LinkedHashMap();
    private boolean pendingTagClose = false;
    private Set imports;
    private List vars;
    private LinkedList configs;
    private Set varsWithXmlValue;

    private final Deque elementPath = new LinkedList();
    private final ExmlSourceFile exmlSourceFile;
    private final ExmlModel exmlModel;
    private String currentConfigName;
    private String currentVarName;
    private boolean isPublicApi;
    private String baseClass;
    private int nsCount = 0;
    private boolean printedRootElement;

    public ExmlToMxmlHandler(ExmlSourceFile exmlSourceFile, ExmlModel exmlModel, PrintStream out) {
      this.exmlSourceFile = exmlSourceFile;
      this.exmlModel = exmlModel;
      currentOut = this.out = out;
    }

    @Override
    public void startDocument() throws SAXException {
      currentOut.println("");
      imports = new LinkedHashSet();
      vars = new ArrayList();
      varsWithXmlValue = new HashSet();
      configs = new LinkedList();
      startRecordingCharacters();

      // add local namespace last to match the local package
      prefixMappings.put(LOCAL_NAMESPACE, createPackageNamespace(exmlModel.getPackageName()));

      super.startDocument();
    }

    @Override
    public void setDocumentLocator(Locator locator) {
      this.locator = locator;
    }

    @Override
    public void startPrefixMapping(String key, String uriValue) throws SAXException {
      prefixMappings.put(key, uriValue);
    }

    public void startElement(String uri, String localName, final String originalQName, Attributes atts) throws SAXException {
      String qName = originalQName;
      Map attributes = new LinkedHashMap();
      if (ExmlUtils.isExmlNamespace(uri)) {
        if (Exmlc.EXML_ROOT_NODE_NAMES.contains(localName)) {
          handleRootNode(atts);
          originalRootName = originalQName;
          originalRootUri = uri;
          qName = null;
        } else if (Exmlc.EXML_ANNOTATION_NODE_NAME.equals(localName)) {
          inMetaData = true;
          qName = null;
        } else if (Exmlc.EXML_CFG_NODE_NAME.equals(localName)) {
          qName = handleCfg(atts);
        } else if (Exmlc.EXML_CONSTANT_NODE_NAME.equals(localName)) {
          // ExmlModel is used for constants
          qName = null;
        } else if (Exmlc.EXML_VAR_NODE_NAME.equals(localName)) {
          qName = handleVar(atts);
        } else if (Exmlc.EXML_DECLARATION_VALUE_NODE_NAME.equals(localName)) {
          qName = handleInnerElement();
        } else if (Exmlc.EXML_IMPORT_NODE_NAME.equals(localName)) {
          qName = handleImport(atts);
        } else if (Exmlc.EXML_CFG_DEFAULT_NODE_NAME.equals(localName)) {
          inConfigDefault = true;
          qName = handleInnerElement();
        } else if (Exmlc.EXML_DESCRIPTION_NODE_NAME.equals(localName)) {
          inConfigDescription = currentConfigName != null;
          qName = null;
        } else if (Exmlc.EXML_OBJECT_NODE_NAME.equals(localName)) {
          // suppress empty s; they only contain code which is simply wrapped by { ... } in MXML:
          if (atts.getLength() == 0 && printedRootElement) {
            qName = null;
          }
          insideExmlObject = true;
        }
      }
      boolean isSubElement = elementPath.size() > 1;
      boolean isPropertyElement = isSubElement && elementPath.size() % 2 == 0;
      if (isPropertyElement) {
        String lastQName = elementPath.peek().newName;
        if (lastQName != null) {
          String prefix = parsePrefixAndLocalName(lastQName)[0];
          String targetName = getTargetClassAttributeName(uri, elementPath.peek().originalName, localName);
          if (targetName.length() > 0) {
            qName = qName(prefix, targetName);
          } else {
            // attribute should be removed
            qName = null;
          }
        }
      } else if (elementPath.size() > 0 && !ExmlUtils.isExmlNamespace(uri)) {
        qName = getTargetClassElementQName(uri, qName);
      }
      if (isNewRoot(uri, originalQName)) {
        addNamespaceMappings(attributes);
      }
      if (qName != null && currentVarName != null) {
        attributes.put("id", currentVarName);
        currentVarName = null;
      }
      boolean indentAttributes = true;
      if (qName != null && currentConfigName != null && !"Array".equals(configs.getLast().getType())) {
        attributes.put("id", currentConfigName);
        indentAttributes = false;
      }
      if (inConfigDefault && qName != null && !configDefaultTypes.containsKey(currentConfigName)) {
        configDefaultTypes.put(currentConfigName, originalQName);
      }
      for (int i = 0; i < atts.getLength(); ++i) {
        String attributeName = atts.getQName(i);
        if (Exmlc.EXML_BASE_CLASS_ATTRIBUTE.equals(attributeName) ||
                Exmlc.EXML_PUBLIC_API_ATTRIBUTE.equals(attributeName)) {
          continue;
        }
        if (isPropertyElement && Exmlc.EXML_MODE_ATTRIBUTE.equals(attributeName)) {
          attributeName = qName(exmlPrefix, Exmlc.EXML_MODE_ATTRIBUTE);
        } else if (!isPropertyElement) {
          attributeName = getTargetClassAttributeName(uri, originalQName, attributeName);
        }
        if ("id".equals(attributeName)) {
          attributeName = "id_";
        }

        if (attributeName.length() > 0) {
          attributes.put(attributeName, atts.getValue(i));
        }
      }
      if (!elementPath.isEmpty() && elementPath.peek().newName == null) {
        if (!inConfigDefault || (ExmlUtils.isExmlNamespace(uri) && Exmlc.EXML_CFG_DEFAULT_NODE_NAME.equals(localName))) {
          popRecordedCharacters();
        }
      }
      if (inConfigDescription || inMetaData) {
        startRecordingCharacters();
      }
      if ("exml:object".equals(qName)) {
        qName = "fx:Object";
      }
      if (isObjectLevel() && qName != null) {
        qName = getMappedComponentName(uri, qName, originalQName);
      }
      if (qName != null) {
        if (isNewRoot(uri, originalQName) && baseClass != null) {
          qName = baseClass;
        }
        printStartTag(uri, originalQName, qName, attributes, indentAttributes);
      }
      if (isNewRoot(uri, originalQName)) {
        flushPendingTagClose();
        imports.add(exmlSourceFile.getConfigClassName());
        addImportsForConfigs();
        addImportsForConstants();
        printHeader();
        printedRootElement = true;
      }
      elementPath.push(new PathElement(originalQName, qName));
      lastColumn = locator.getColumnNumber();
    }

    private void addNamespaceMappings(Map attributes) {
      Map mxmlPrefixMappings = new LinkedHashMap();
      mxmlPrefixMappings.put("fx", MxmlUtils.MXML_NAMESPACE_URI);
      exmlPrefix = findPrefix(Exmlc.EXML_NAMESPACE_URI);
      if (exmlPrefix == null) {
        System.err.println("[WARN] ExmlToMxml: no EXML namespace found!");
        exmlPrefix = "exml";
        mxmlPrefixMappings.put("exml", Exmlc.EXML_NAMESPACE_URI);
      }

      mxmlPrefixMappings.putAll(prefixMappings);

      for (Map.Entry prefixMapping : mxmlPrefixMappings.entrySet()) {
        String key = prefixMapping.getKey();
        attributes.put(key.isEmpty() ? "xmlns" : "xmlns:" + key, prefixMapping.getValue());
      }
    }

    private boolean isObjectLevel() {
      return elementPath.size() % 2 == 1;
    }

    private void printStartTag(String uri, String originalQName, String qName, Map attributes, boolean indentAttributes) {
      flush();
      String originalName = isNewRoot(uri, originalQName) ? originalRootName : originalQName;
      int column = isNewRoot(uri, originalQName) ? 1 : lastColumn;
      int indent = column + originalName.length();
      int qNameLengthDelta = originalName.length() - qName.length();
      String firstIndent = qNameLengthDelta == 0 || attributes.size() <= 1
              ? ""
              : qNameLengthDelta > 0
              ? StringUtils.repeat(" ", qNameLengthDelta)
              : String.format("%n%s", StringUtils.repeat(" ", indent));
      currentOut.printf("<%s%s", qName, firstIndent);
      printAttributes(attributes, indentAttributes ? indent + 1 : 0);
      pendingTagClose = true;
    }

    private void printHeader() throws SAXException {
      printMetadata();
      printOpenScript();
      printImports();
      printConstants();
      printVars();
      printInitializer();
      printConstructor();
      printConfigVars();
      printCloseScript();
      printDeclarations();
    }

    private void printImports() {
      for (String anImport : imports) {
        currentOut.printf("    import %s;%n", anImport);
      }
      if (!imports.isEmpty()) {
        currentOut.println();
      }
    }

    private void printConstants() {
      if (Exmlc.EXML_COMPONENT_NODE_NAME.equals(parsePrefixAndLocalName(originalRootName)[1])) {
        currentOut.printf(
                PUBLIC_STATIC_CONST_DECLARATION_FORMAT,
                "xtype", "String", CompilerUtils.quote(exmlModel.getConfigClass().getFullName()));
      }
      for (Declaration constant : exmlModel.getConfigClass().getConstants()) {
        printASDoc(constant.getDescription());
        currentOut.printf(
                PUBLIC_STATIC_CONST_DECLARATION_FORMAT,
                constant.getName(), constant.getType(), constant.getValue()
        );
      }
    }

    private void addImportsForConfigs() throws SAXException {
      for (Declaration config : configs) {
        String defaultType = configDefaultTypes.get(config.getName());
        if (defaultType != null && !config.getType().equals(defaultType)) {
          addImport(config.getType());
        }
        if (!hasDefaultConstructor(config.getType())) {
          addImport(config.getType());
        }
      }
    }

    private void addImportsForConstants() throws SAXException {
      for (Declaration constant : exmlModel.getConfigClass().getConstants()) {
        addImport(constant.getType());
      }
    }

    private boolean hasDefaultConstructor(String name) throws SAXException {
      String qname = resolveQName(name);
      if (AS3Type.typeByName(qname) != null) {
        return true;
      }

      CompilationUnit compilationUnit = configClassRegistry.getJangarooParser().getCompilationUnit(qname);
      if (compilationUnit != null) {
        try {
          CompilationUnitModel compilationUnitModel = new ApiModelGenerator(false).generateModel(compilationUnit);
          if (compilationUnitModel.getClassModel().isInterface()) {
            return false;
          }
          MethodModel constructor = compilationUnitModel.getClassModel().getConstructor();
          if (constructor != null) {
            List params = constructor.getParams();
            return params.size() == 0 || params.get(0).isOptional() || params.get(0).isRest() || params.get(0).getName().equals("config");
          }
        } catch (IOException e) {
          throw new SAXException(e);
        }
      }
      return true;
    }

    private String getConfigClassFromTargetClass(String name) throws SAXException {
      CompilationUnit compilationUnit = configClassRegistry.getJangarooParser().getCompilationUnit(resolveQName(name));
      if (compilationUnit != null) {
        try {
          CompilationUnitModel compilationUnitModel = new ApiModelGenerator(false).generateModel(compilationUnit);
          MethodModel constructor = compilationUnitModel.getClassModel().getConstructor();
          if (constructor != null && constructor.getParams().size() > 0) {
            String type = constructor.getParams().get(0).getType();
            if (type != null && CompilerUtils.packageName(type).isEmpty()) {
              for (AstNode astNode : compilationUnit.getDirectives()) {
                if (astNode instanceof ImportDirective) {
                  ImportDirective importDirective = (ImportDirective) astNode;
                  if (CompilerUtils.className(importDirective.getQualifiedName()).equals(type)) {
                    return importDirective.getQualifiedName();
                  }
                }
              }
            }
            return type;
          }
        } catch (IOException e) {
          throw new SAXException(e);
        }
      }
      return null;
    }

    private String resolveQName(String name) {
      if (!name.contains(".")) {
        for (String anImport : imports) {
          if (CompilerUtils.className(anImport).equals(name)) {
            return anImport;
          }
        }
      }
      return name;
    }

    private String resolveQNameFromModel(String name) {
      if (!name.contains(".")) {
        for (String anImport : exmlModel.getImports()) {
          if (CompilerUtils.className(anImport).equals(name)) {
            return anImport;
          }
        }
        String localQName = CompilerUtils.qName(exmlModel.getPackageName(), name);
        if (configClassRegistry.getJangarooParser().getCompilationUnit(localQName) != null) {
          return localQName;
        }
      }
      return name;
    }

    private boolean isNewRoot(String uri, String qName) {
      if (qName == null || elementPath.size() != 1) {
        return false;
      }
      String localName = parsePrefixAndLocalName(qName)[1];
      return !ExmlUtils.isExmlNamespace(uri) || Exmlc.EXML_OBJECT_NODE_NAME.equals(localName);
    }

    private void printConfigVars() throws SAXException {
      for (Declaration config : configs) {
        String defaultType = configDefaultTypes.get(config.getName());
        if (!"Array".equals(config.getType()) && (!hasDefaultConstructor(config.getType()) || (defaultType != null && !config.getType().equals(defaultType)))) {
          currentOut.printf("%n");
          if (!hasDefaultConstructor(config.getType())) {
            printASDoc(config.getDescription());
          }
          currentOut.printf("    [Bindable]%n");
          currentOut.printf("    public var %s:%s", config.getName(), config.getType());
          if (config.getValue() != null) {
            currentOut.printf(" = %s", MxmlUtils.getBindingExpression(config.getValue()).trim());
          }
          currentOut.printf(";%n");
        }
      }
    }

    private void printConstructor() {
      String configClassName = CompilerUtils.className(exmlSourceFile.getConfigClassName());
      currentOut.printf("    public native function %s(config:%s = null);%n", exmlModel.getClassName(), configClassName);
    }

    private void printInitializer() {
      String configClassName = CompilerUtils.className(exmlSourceFile.getConfigClassName());
      if (varsWithXmlValue.size() != vars.size()) {
        currentOut.printf("    // called by generated constructor code%n");
        currentOut.printf("    private function __initialize__(config:%s):void {%n", configClassName);
        for (Declaration var : vars) {
          if (!(varsWithXmlValue.contains(var.getName()))) {
            currentOut.printf("      %s = %s;%n", var.getName(), formatValue(var.getValue(), var.getType()));
          }
        }
        currentOut.printf("    }%n%n");
      }
    }

    private void printVars() {
      String configClassName = CompilerUtils.className(exmlSourceFile.getConfigClassName());
      currentOut.printf("    private var config:%s;%n", configClassName);

      for (Declaration var : vars) {
        String type = var.getType();
        if (type == null || type.isEmpty()) {
          type = "*";
        }
        currentOut.printf("    private var %s:%s;%n", var.getName(), type);
      }
      currentOut.println();
    }

    private void printDeclarations() throws SAXException {
      boolean hasDeclarations = false;
      String targetClassQName = exmlSourceFile.getTargetClassName();
      String targetClassName = CompilerUtils.className(targetClassQName);

      for (Declaration var : vars) {
        if (varsWithXmlValue.contains(var.getName()) && !"".equals(var.getValue())) {
          if (!hasDeclarations) {
            printOpenDeclarations();
            hasDeclarations = true;
          }
          currentOut.printf("%n    %s", var.getValue());
        }
      }

      for (Declaration config : configs) {
        if (!hasDefaultConstructor(config.getType())) {
          continue;
        }

        QName qName = findTypeAndPrefix(config.getType());
        String type = qName.getLocalPart();
        String prefix = qName.getPrefix();

        if (!hasDeclarations) {
          printOpenDeclarations();
          hasDeclarations = true;
        } else {
          currentOut.printf("%n");
        }
        if (config.getDescription() != null) {
          currentOut.printf("%n    ");
          printComment(config.getDescription());
        }
        currentOut.printf("%n    ");

        if (configDefaultSubElements.containsKey(config.getName()) && !"Array".equals(config.getType())) {
          currentOut.printf(configDefaultSubElements.get(config.getName()));
        } else {
          currentOut.printf("<%s id=\"%s\"", qName(prefix, type), config.getName());

          String value = config.getValue();
          if (value != null) {
            currentOut.printf(">%s", value, qName(prefix, type));
          } else if (configDefaultSubElements.containsKey(config.getName())) {
            // Array
            currentOut.printf(">%n      ");
            currentOut.printf(configDefaultSubElements.get(config.getName()));
            currentOut.printf("%n    ", qName(prefix, type));
          } else {
            currentOut.printf("/>");
          }
        }
      }

      if (hasDeclarations) {
        printCloseDeclarations();
      } else {
        currentOut.println();
      }
    }

    private void printOpenDeclarations() {
      currentOut.printf("%n  ");
    }

    private void printCloseDeclarations() {
      currentOut.printf("%n  %n");
    }

    private QName findTypeAndPrefix(String type) throws SAXException {
      type = "*".equals(type) ? "Object" : type;
      String prefix;
      List qNames = mxmlComponentRegistry.getQNamesByClassName(resolveQName(type));
      if (qNames != null) {
        QName qName = findQName(CompilerUtils.className(type), qNames);
        prefix = findPrefix(qName.getNamespaceURI());
        type = qName.getLocalPart();
      } else {
        prefix = findPrefixForType(type);
        type = CompilerUtils.className(type);
      }
      return new QName(null, type, prefix == null ? "" : prefix);
    }

    private void printASDoc(String text) {
      if (text != null) {
        currentOut.printf("    /**%n");
        for (String line : text.trim().split("[\n\r]")) {
          currentOut.printf("     * %s%n", line.trim());
        }
        currentOut.printf("     */%n");
      }
    }

    private void printMetadata() {
      if (isPublicApi) {
        currentOut.printf("%n  [%s]", Jooc.PUBLIC_API_INCLUSION_ANNOTATION_NAME);
      }
      for (String md : metaData) {
        currentOut.printf("%n  [%s]", md);
      }
    }

    private void printOpenScript() {
      currentOut.printf(OPEN_FX_SCRIPT);
    }

    private void printCloseScript() {
      currentOut.printf(CLOSE_FX_SCRIPT);
    }

    private String getMappedComponentName(String uri, String qName, String originalQName) {
      ConfigClass configClass = getConfigClass(uri, originalQName);
      if (configClass != null) {
        String componentClassName = configClass.getComponentClassName();
        String mappedClassName = (String) migrationMap.get(componentClassName);
        List qNames = mxmlComponentRegistry.getQNamesByClassName(mappedClassName != null ? mappedClassName : componentClassName);
        if (qNames != null) {
          return findQName(originalQName, qNames).getLocalPart();
        }
      }
      return qName;
    }

    private QName findQName(String originalQName, List qNames) {
      for (QName name : qNames) {
        if (name.getLocalPart().toLowerCase().equals(originalQName.toLowerCase())) {
          return name;
        }
      }
      return qNames.get(qNames.size() - 1);
    }

    private String findPrefixForPackage(String packageName, boolean configUriOnly) {
      if (packageName.isEmpty()) {
        return "fx";
      }
      String prefix = findPrefix(Exmlc.EXML_CONFIG_URI_PREFIX + packageName);
      return prefix == null && !configUriOnly ? findPrefix(createPackageNamespace(packageName)) : prefix;
    }

    private String findPrefix(String namespace) {
      for (Map.Entry prefixMapping : prefixMappings.entrySet()) {
        if (namespace.equals(prefixMapping.getValue())) {
          return prefixMapping.getKey();
        }
      }
      return null;
    }

    private String createPackageNamespace(String packageName) {
      return CompilerUtils.qName(packageName, "*");
    }

    private void printAttributes(Map attributes, int indent) {
      String whitespace = " ";
      for (Map.Entry attribute : attributes.entrySet()) {
        String value = attribute.getValue();
        value = convertNewLines(CompilerUtils.denormalizeAttributeValue(value));
        if (!ExmlUtils.isCodeExpression(value)) {
          // escape all opening curly braces, as MXML also recognizes them anywhere inside a string:
          value = value.replaceAll("\\{", "\\\\{");
        }
        value = escapeXml(value);
        String quotes;
        if (value.contains("\"") && !value.contains("'")) {
          quotes = "'";
        } else {
          quotes = "\"";
          value = value.replaceAll("\"", """);
        }
        currentOut.printf("%s%s=%s%s%s", whitespace, attribute.getKey(), quotes, value, quotes);
        if (indent > 0) {
          whitespace = String.format("%n%s", StringUtils.repeat(" ", indent));
        }
      }
    }


    private String escapeXml(String xmlString) {
      return xmlString.replaceAll("&", "&").replaceAll("<", "<");
    }

    private String handleInnerElement() {
      elementRecorder = new ByteArrayOutputStream();
      try {
        currentOut = new PrintStream(elementRecorder, false, "UTF-8");
      } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e);
      }
      if (currentVarName != null) {
        Declaration currentVar = vars.get(vars.size() - 1);
        if ("Array".equals(currentVar.getType())) {
          return "fx:Array";
        }
      }
      return null; // do not render default element (but its contents!)
    }

    private String handleImport(Attributes atts) {
      String importedClassName = atts.getValue(Exmlc.EXML_IMPORT_CLASS_ATTRIBUTE);
      if (importedClassName != null) {
        imports.add(importedClassName);
      }
      return null; // do not render import elements
    }

    private Declaration createDeclaration(Attributes atts) {
      String name = atts.getValue(Exmlc.EXML_DECLARATION_NAME_ATTRIBUTE);
      String type = atts.getValue(Exmlc.EXML_DECLARATION_TYPE_ATTRIBUTE);
      String value = atts.getValue(Exmlc.EXML_DECLARATION_VALUE_ATTRIBUTE);
      return new Declaration(name, value, type);
    }

    private void addImport(String type) {
      String packageName = CompilerUtils.packageName(type);
      if (!packageName.isEmpty()) {
        if (findPrefixForPackage(packageName, true) == null) {
          imports.add(type);
        }
      }
    }

    private String handleVar(Attributes atts) {
      Declaration declaration = createDeclaration(atts);
      vars.add(declaration);
      addImport(declaration.getType());
      currentVarName = declaration.getName();
      return null; // do not render var elements
    }

    private String handleCfg(Attributes atts) throws SAXException {
      Declaration declaration = createDeclaration(atts);
      currentConfigName = declaration.getName();
      if (declaration.getType() != null) {
        // even if this config does not add anything to the target class,
        // its type must be imported, because other code may take advantage of this import.
        addImport(declaration.getType());
        String mappedClassName = getMappedClassName(declaration.getType());
        if (mappedClassName != null) {
          declaration = new Declaration(declaration.getName(), declaration.getValue(), mappedClassName);
        }
      }
      configs.add(declaration);
      if (hasDefaultConstructor(declaration.getType())) {
        addPrefixMapping(declaration.getType());
      }
      String configDefault = atts.getValue("default");
      if (configDefault != null && !configDefault.isEmpty()) {
        declaration.setValue(configDefault);
      }
      return null; // do not render config elements
    }

    private String addPrefixMapping(String type) throws SAXException {
      String prefix = findPrefixForType(type);
      if (prefix == null) {
        prefix = createPrefixMapping(createPackageNamespace(CompilerUtils.packageName(resolveQName(type))));
      }
      return prefix;
    }

    private String createPrefixMapping(String namespace) throws SAXException {
      String prefix = "ns" + (++nsCount);
      prefixMappings.put(prefix, namespace);
      return prefix;
    }

    private String findPrefixForType(String type) throws SAXException {
      String qName = resolveQName(type);
      String packageName = CompilerUtils.packageName(qName);
      String prefix = findPrefixForPackage(packageName, false);
      if (prefix == null) {
        ConfigClass configClass = configClassRegistry.getConfigClassByName(qName);
        if (configClass != null) {
          qName = configClass.getComponentClassName();
          packageName = CompilerUtils.packageName(qName);
          prefix = findPrefixForPackage(packageName, false);
        } else {
          String guessedConfigClassName = getConfigClassFromTargetClass(qName);
          if (guessedConfigClassName != null) {
            configClass = configClassRegistry.getConfigClassByName(guessedConfigClassName);
            if (configClass != null && configClass.getComponentClassName().equals(qName)) {
              packageName = CompilerUtils.packageName(guessedConfigClassName);
              prefix = findPrefixForPackage(packageName, false);
            }
          }
        }
      }
      return prefix;
    }

    private void handleRootNode(Attributes atts) throws SAXException {
      String asDoc = exmlModel.getDescription();
      if (asDoc != null && !asDoc.trim().isEmpty()) {
        printComment(asDoc);
        currentOut.println();
      }

      baseClass = atts.getValue(Exmlc.EXML_BASE_CLASS_ATTRIBUTE);
      if (baseClass != null && !baseClass.isEmpty()) {
        // baseClass attribute has been specified, so the super class of the component is actually that:
        baseClass = qName(addPrefixMapping(resolveQNameFromModel(baseClass)), CompilerUtils.className(baseClass));
      }

      String publicApiValue = atts.getValue(Exmlc.EXML_PUBLIC_API_ATTRIBUTE);
      if (publicApiValue != null && !publicApiValue.isEmpty()) {
        PublicApiMode publicApiMode = Exmlc.parsePublicApiMode(publicApiValue);
        if (publicApiMode != PublicApiMode.FALSE) {
          isPublicApi = true;
        }
      }
    }

    private void printComment(String text) {
      currentOut.print("");
    }

    private String formatValue(String value, String type) {
      return value == null ? null
              : JsonObject.valueToString(ExmlToModelParser.getAttributeValue(value, type), 2, 4);
    }

    @Override
    public void characters(char[] ch, int start, int length) throws SAXException {
      flushPendingTagClose();
      String cdata = new String(ch, start, length);
      if (!(elementPath.size() == 2 && elementPath.peek().newName == null && isNotEmptyText(cdata.trim())) || inMetaData) {
        if (insideCdata) {
          cdata = escapeXml(cdata);
        }
        super.characters(cdata.toCharArray(), 0, cdata.length());
      }
      lastColumn = locator.getColumnNumber();
    }

    @Override
    public void endElement(String uri, String localName, String qName) throws SAXException {
      if (elementPath.size() == 1) {
        // ignoring the root element
        return;
      }
      qName = elementPath.pop().newName;
      if (inMetaData) {
        inMetaData = false;
        metaData.add(popRecordedCharacters());
      } else if (inConfigDescription) {
        inConfigDescription = false;
        configs.getLast().setDescription(popRecordedCharacters());
      }
      if (qName != null) {
        if (pendingTagClose) {
          currentOut.print("/>");
          pendingTagClose = false;
        } else {
          flush();
          if (elementPath.size() == 1) {
            currentOut.printf("%n");
          }
          currentOut.printf("", qName);
        }
      } else if (insideExmlObject) {
        flush();
      }
      insideExmlObject = false; // s cannot be nested.
      startRecordingCharacters();
      if (ExmlUtils.isExmlNamespace(uri)) {
        if (Exmlc.EXML_CFG_DEFAULT_NODE_NAME.equals(localName) ||
                Exmlc.EXML_DECLARATION_VALUE_NODE_NAME.equals(localName)) {
          String value = elementRecorder.toString();
          if (currentConfigName != null) {
            configDefaultSubElements.put(currentConfigName, value);
          } else if (!vars.isEmpty()) {
            Declaration varDeclaration = vars.get(vars.size() - 1);
            varDeclaration.setValue(value);
            varsWithXmlValue.add(varDeclaration.getName());
          }
          currentOut.close();
          currentOut = out;
          elementRecorder = null;
          inConfigDefault = false;
        } else if (Exmlc.EXML_CFG_NODE_NAME.equals(localName)) {
          currentConfigName = null;
        } else if (Exmlc.EXML_VAR_NODE_NAME.equals(localName)) {
          currentVarName = null;
        }
      }
      lastColumn = locator.getColumnNumber();
    }

    private String getTargetClassElementQName(String uri, String qName) {
      ConfigClass configClass = getConfigClass(uri, qName);
      if (configClass != null) {
        String targetClassName = configClass.getComponentClassName();
        if (targetClassName != null) {
          if (migrationMap.containsKey(targetClassName)) {
            targetClassName = (String) migrationMap.get(targetClassName);
          }
          return formatQName(parsePrefixAndLocalName(qName)[0], CompilerUtils.className(targetClassName));
        }
      }
      return null;
    }

    private String getTargetClassAttributeName(String uri, String qName, String attributeName) {
      ConfigClass configClass = getConfigClass(uri, qName);
      while (configClass != null) {
        String classAndAttribute = configClass.getFullName() + "#" + attributeName;
        if (migrationMap.containsKey(classAndAttribute)) {
          String targetName = (String) migrationMap.get(classAndAttribute);
          return targetName.substring(targetName.indexOf('#') + 1);
        } else {
          configClass = configClass.getSuperClass();
        }
      }
      return attributeName;
    }

    private String getMappedClassName(String type) {
      String mappedClassName = null;
      ConfigClass configClass = configClassRegistry.getConfigClassByName(resolveQName(type));
      if (configClass != null) {
        mappedClassName = configClass.getComponentClassName();
        if (migrationMap.containsKey(mappedClassName)) {
          mappedClassName = (String) migrationMap.get(mappedClassName);
        }
      }
      return mappedClassName;
    }

    private ConfigClass getConfigClass(String uri, String qName) {
      String configClassName = getClassName(uri, qName);
      return configClassName == null ? null : configClassRegistry.getConfigClassByName(configClassName);
    }

    private String getClassName(String uri, String qName) {
      String packageName = ExmlUtils.parsePackageFromNamespace(uri);
      return packageName == null ? null : CompilerUtils.qName(packageName, parsePrefixAndLocalName(qName)[1]);
    }

    @Override
    public void endDocument() throws SAXException {
      if (!printedRootElement && ExmlUtils.isExmlNamespace(originalRootUri)) {
        ConfigClassType configClassType = ConfigClassType.fromExmlRootNodeName(parsePrefixAndLocalName(originalRootName)[1]);
        String superClassName = configClassType.getDefaultSuperConfigClassName();
        String localName = configClassType == ConfigClassType.CLASS ? Exmlc.EXML_OBJECT_NODE_NAME : CompilerUtils.className(superClassName);
        String prefix = configClassType == ConfigClassType.CLASS ? findPrefix(originalRootUri) : findPrefixForType(superClassName);
        if (prefix == null) {
          prefix = createPrefixMapping("exml:ext.config");
        }
        String namespace = prefixMappings.get(prefix);
        startElement(namespace, localName, qName(prefix, localName), new AttributesImpl());
        endElement(namespace, localName, qName(prefix, localName));
      }
      // add last new-line to end up with a correct text file format:
      currentOut.println();
    }

    @Override
    public void startDTD(String name, String publicId, String systemId) throws SAXException {

    }

    @Override
    public void endDTD() throws SAXException {

    }

    @Override
    public void startEntity(String name) throws SAXException {

    }

    @Override
    public void endEntity(String name) throws SAXException {

    }

    @Override
    public void startCDATA() throws SAXException {
      insideCdata = true;
      lastColumn = locator.getColumnNumber();
    }

    @Override
    public void endCDATA() throws SAXException {
      insideCdata = false;
      lastColumn = locator.getColumnNumber();
    }

    @Override
    public void comment(char[] ch, int start, int length) throws SAXException {
      flush();
      currentOut.print("");
      lastColumn = locator.getColumnNumber();
    }

    private void flushPendingTagClose() {
      if (pendingTagClose) {
        currentOut.print(">");
        pendingTagClose = false;
      }
    }

    private String qName(String prefix, String localName) {
      return prefix + (prefix.isEmpty() ? "" : ":") + localName;
    }

    private void flush() {
      flushPendingTagClose();
      String recordedCharacters = popRecordedCharacters();
      if (recordedCharacters != null) {
        String output = convertNewLines(recordedCharacters);
        if (insideExmlObject) {
          if (!output.trim().isEmpty()) {
            output = MxmlUtils.createBindingExpression(output);
          }
        }
        currentOut.print(output);
      }
      startRecordingCharacters();
    }
  }

  private static String convertNewLines(String output) {
    return output.replaceAll("\n", System.getProperty("line.separator"));
  }

  private static String[] parsePrefixAndLocalName(String qName) {
    int colonPos = qName.indexOf(':');
    String prefix = colonPos == -1 ? "" : qName.substring(0, colonPos);
    String localName = qName.substring(colonPos + 1);
    return new String[]{prefix, localName};
  }

  private static String formatQName(String prefix, String localName) {
    return prefix == null || prefix.isEmpty() ? localName : String.format("%s:%s", prefix, localName);
  }

  public static boolean isNotEmptyText(String str) {
    if (str == null) {
      return false;
    }
    int sz = str.length();
    for (int i = 0; i < sz; i++) {
      if (Character.isLetterOrDigit(str.charAt(i))) {
        return true;
      }
    }
    return false;
  }

  private static class PathElement {
    String originalName;
    String newName;

    public PathElement(String originalName, String newName) {
      this.originalName = originalName;
      this.newName = newName;
    }
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy