package com.intellij.flex.build; import com.intellij.openapi.util.JDOMUtil; import com.intellij.openapi.util.io.FileUtil; import com.intellij.util.ArrayUtil; import gnu.trove.THashSet; import org.jdom.Attribute; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.Namespace; import java.io.File; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; import java.util.List; public class FlexCompilerConfigFileUtilBase { public static final String FLEX_CONFIG = "flex-config"; public static final String COMPILER = "compiler"; public static final String EXTERNAL_LIBRARY_PATH = "external-library-path"; public static final String INCLUDE_LIBRARIES = "include-libraries"; public static final String LIBRARY = "library"; public static final String LIBRARY_PATH = "library-path"; public static final String PATH_ELEMENT = "path-element"; public static final String APPEND = "append"; private static final String[] ELEMENTS_TO_REMOVE = { // these 8 options are for SWC compilation only, not applicable for SWF compilation "compute-digest", "directory", "include-classes", "include-file", "include-lookup-only", "include-namespaces", "include-sources", "include-stylesheet", // main class and output path are taken according to run configuration, i.e. as set in generated config file text /*"debug",*/ "file-specs", "output", // link-report option in custom config file is actual only for main app. Need to remove it for RLMs, tests, etc. "link-report", // load-externs option should not be used because it can lead to runtime errors like IDEA-70155 "load-externs" }; private static final String[] OPTIONS_CONTAINING_PATHS = {"path-element", "manifest", "defaults-css-url", "filename", "link-report", "load-externs", "services", "resource-bundle-list"}; private static final String[] NON_REPEATABLE_OPTIONS_THAT_CAN_BE_IN_GENERATED_FILE = {"mobile", "preloader", "warn-no-constructor", "accessible", "keep-generated-actionscript", "services", "context-root", "defaults-css-url", "debug", "target-player", "swf-version", "static-link-runtime-shared-libraries", "date", "title", "language", "contributor", "creator", "publisher", "description", "manager-class"}; public static String mergeWithCustomConfigFile(final String generatedConfigText, final String additionalConfigFilePath, final boolean makeExternalLibsMerged, final boolean makeIncludedLibsMerged) { final File additionalConfigFile = new File(additionalConfigFilePath); if (!additionalConfigFile.isFile()) { return generatedConfigText; } final Element rootElement; try { rootElement = JDOMUtil.load(additionalConfigFile); } catch (JDOMException | IOException e) { return generatedConfigText; } if (!FLEX_CONFIG.equals(rootElement.getName())) { return generatedConfigText; } removeSwcSpecificElementsRecursively(rootElement); makeLibrariesMergedIntoCode(rootElement, makeExternalLibsMerged, makeIncludedLibsMerged); try { final Element otherRootElement = JDOMUtil.load(generatedConfigText); assert FLEX_CONFIG.equals(rootElement.getName()) : JDOMUtil.writeElement(rootElement); appendDocument(rootElement, otherRootElement); } catch (IOException | JDOMException e) { assert false : e.getMessage() + "\n" + generatedConfigText; } return JDOMUtil.writeElement(rootElement); } private static void removeSwcSpecificElementsRecursively(final Element element) { for (final String elementName : ELEMENTS_TO_REMOVE) { element.removeChildren(elementName, element.getNamespace()); } for (final Element child : element.getChildren()) { removeSwcSpecificElementsRecursively(child); } } private static void makeLibrariesMergedIntoCode(final Element rootElement, final boolean externalLibs, final boolean includedLibs) { final Namespace namespace = rootElement.getNamespace(); final Collection<String> paths = removeLibs(rootElement, externalLibs, includedLibs); if (!paths.isEmpty()) { final Element compilerElement = rootElement.getChild(COMPILER, namespace); Element libraryPathElement = compilerElement.getChild(LIBRARY_PATH, namespace); if (libraryPathElement == null) { libraryPathElement = new Element(LIBRARY_PATH, namespace); libraryPathElement.setAttribute(new Attribute(APPEND, "true")); compilerElement.addContent(libraryPathElement); } for (final String path : paths) { final Element pathElement = new Element(PATH_ELEMENT, namespace); pathElement.addContent(path); libraryPathElement.addContent(pathElement); } } } private static Collection<String> removeLibs(final Element rootElement, final boolean removeExternal, final boolean removeIncluded) { final Namespace namespace = rootElement.getNamespace(); final Collection<String> result = new ArrayList<>(); //noinspection unchecked for (Element compilerElement : rootElement.getChildren(COMPILER, namespace)) { if (removeExternal) { //noinspection unchecked for (Element externalLibraryPathElement : compilerElement.getChildren(EXTERNAL_LIBRARY_PATH, namespace)) { final Collection<Element> pathElementsToRemove = new ArrayList<>(); //noinspection unchecked for (Element pathElement : externalLibraryPathElement.getChildren(PATH_ELEMENT, namespace)) { final String path = pathElement.getText(); final String fileName = path.substring(FileUtil.toSystemIndependentName(path).lastIndexOf("/") + 1); if (fileName.startsWith("playerglobal") || fileName.startsWith("airglobal")) { continue; } result.add(path); pathElementsToRemove.add(pathElement); } for (Element pathElement : pathElementsToRemove) { externalLibraryPathElement.removeContent(pathElement); } } } if (removeIncluded) { //noinspection unchecked for (Element includeLibrariesElement : compilerElement.getChildren(INCLUDE_LIBRARIES, namespace)) { final Collection<Element> libraryElementsToRemove = new ArrayList<>(); //noinspection unchecked for (Element libraryElement : includeLibrariesElement.getChildren(LIBRARY, namespace)) { result.add(libraryElement.getText()); libraryElementsToRemove.add(libraryElement); } for (Element pathElement : libraryElementsToRemove) { includeLibrariesElement.removeContent(pathElement); } } } } return result; } private static void appendDocument(final Element rootElement, final Element otherRootElement) { final Collection<Element> toRemove = findDuplicateElementsRecursively(rootElement, otherRootElement); for (Element duplicateElement : toRemove) { final Element parentElement = duplicateElement.getParentElement(); parentElement.removeContent(duplicateElement); if (parentElement.getChildren().isEmpty()) { parentElement.getParentElement().removeContent(parentElement); } } //noinspection unchecked for (final Element otherElement : otherRootElement.getChildren()) { rootElement.addContent(otherElement.clone()); } } // required to avoid setting the same option values twice because it may lead to compilation failure (e.g. if the same locale is listed twice) private static Collection<Element> findDuplicateElementsRecursively(final Element existingElement, final Element otherElement) { final Collection<Element> result = new THashSet<>(); //noinspection unchecked for (Element potentialChild : otherElement.getChildren()) { final List existingChildren = existingElement.getChildren(potentialChild.getName(), existingElement.getNamespace()); //noinspection unchecked for (Element existingChild : (Iterable<Element>)existingChildren) { final String potentialChildContent = potentialChild.getTextTrim(); if (potentialChildContent.isEmpty()) { result.addAll(findDuplicateElementsRecursively(existingChild, potentialChild)); } else { final String existingElementContent = existingChild.getTextTrim(); if (ArrayUtil.contains(existingChild.getName(), NON_REPEATABLE_OPTIONS_THAT_CAN_BE_IN_GENERATED_FILE)) { result.add(potentialChild); } else if (areOptionValuesEqual(existingChild.getName(), potentialChildContent, existingElementContent)) { // remove only similar repeatable values, do not remove equal values of <policy-file-url/> that are for different <runtime-shared-library-path/> elements. if (existingElement.getChildren().size() == existingChildren.size()) { result.add(potentialChild); } } } } } return result; } private static boolean areOptionValuesEqual(final String optionName, final String value1, final String value2) { if (value1.equals(value2)) return true; if (ArrayUtil.contains(optionName, OPTIONS_CONTAINING_PATHS)) { if (FileUtil.toSystemIndependentName(value1).equals(FileUtil.toSystemIndependentName(value2))) return true; } return false; } }