1 /*
   2  * Copyright (c) 2019, 2020, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 package jdk.incubator.jpackage.internal;
  26 
  27 import java.io.IOException;
  28 import java.nio.file.Files;
  29 import java.nio.file.Path;
  30 import java.util.ArrayList;
  31 import java.util.List;
  32 import java.util.Map;
  33 import javax.xml.parsers.DocumentBuilder;
  34 import javax.xml.parsers.DocumentBuilderFactory;
  35 import javax.xml.parsers.ParserConfigurationException;
  36 import javax.xml.xpath.XPath;
  37 import javax.xml.xpath.XPathConstants;
  38 import javax.xml.xpath.XPathExpressionException;
  39 import javax.xml.xpath.XPathFactory;
  40 import org.w3c.dom.Document;
  41 import org.w3c.dom.NodeList;
  42 import org.xml.sax.SAXException;
  43 
  44 import static jdk.incubator.jpackage.internal.StandardBundlerParam.VERSION;
  45 import static jdk.incubator.jpackage.internal.StandardBundlerParam.ADD_LAUNCHERS;
  46 import static jdk.incubator.jpackage.internal.StandardBundlerParam.APP_NAME;
  47 
  48 public class AppImageFile {
  49 
  50     // These values will be loaded from AppImage xml file.
  51     private final String creatorVersion;
  52     private final String creatorPlatform;
  53     private final String launcherName;
  54     private final List<String> addLauncherNames;
  55 
  56     private final static String FILENAME = ".jpackage.xml";
  57 
  58     private final static Map<Platform, String> PLATFORM_LABELS = Map.of(
  59             Platform.LINUX, "linux", Platform.WINDOWS, "windows", Platform.MAC,
  60             "macOS");
  61 
  62 
  63     private AppImageFile() {
  64         this(null, null, null, null);
  65     }
  66 
  67     private AppImageFile(String launcherName, List<String> addLauncherNames,
  68             String creatorVersion, String creatorPlatform) {
  69         this.launcherName = launcherName;
  70         this.addLauncherNames = addLauncherNames;
  71         this.creatorVersion = creatorVersion;
  72         this.creatorPlatform = creatorPlatform;
  73     }
  74 
  75     /**
  76      * Returns list of additional launchers configured for the application.
  77      * Each item in the list is not null or empty string.
  78      * Returns empty list for application without additional launchers.
  79      */
  80     List<String> getAddLauncherNames() {
  81         return addLauncherNames;
  82     }
  83 
  84     /**
  85      * Returns main application launcher name. Never returns null or empty value.
  86      */
  87     String getLauncherName() {
  88         return launcherName;
  89     }
  90 
  91     void verifyCompatible() throws ConfigException {
  92         // Just do nothing for now.
  93     }
  94 
  95     /**
  96      * Returns path to application image info file.
  97      * @param appImageDir - path to application image
  98      */
  99     public static Path getPathInAppImage(Path appImageDir) {
 100         return ApplicationLayout.platformAppImage()
 101                 .resolveAt(appImageDir)
 102                 .appDirectory()
 103                 .resolve(FILENAME);
 104     }
 105 
 106     /**
 107      * Saves file with application image info in application image.
 108      * @param appImageDir - path to application image
 109      * @throws IOException
 110      */
 111     static void save(Path appImageDir, Map<String, Object> params)
 112             throws IOException {
 113         IOUtils.createXml(getPathInAppImage(appImageDir), xml -> {
 114             xml.writeStartElement("jpackage-state");
 115             xml.writeAttribute("version", getVersion());
 116             xml.writeAttribute("platform", getPlatform());
 117 
 118             xml.writeStartElement("app-version");
 119             xml.writeCharacters(VERSION.fetchFrom(params));
 120             xml.writeEndElement();
 121 
 122             xml.writeStartElement("main-launcher");
 123             xml.writeCharacters(APP_NAME.fetchFrom(params));
 124             xml.writeEndElement();
 125 
 126             List<Map<String, ? super Object>> addLaunchers =
 127                 ADD_LAUNCHERS.fetchFrom(params);
 128 
 129             for (int i = 0; i < addLaunchers.size(); i++) {
 130                 Map<String, ? super Object> sl = addLaunchers.get(i);
 131                 xml.writeStartElement("add-launcher");
 132                 xml.writeCharacters(APP_NAME.fetchFrom(sl));
 133                 xml.writeEndElement();
 134             }
 135         });
 136     }
 137 
 138     /**
 139      * Loads application image info from application image.
 140      * @param appImageDir - path to application image
 141      * @return valid info about application image or null
 142      * @throws IOException
 143      */
 144     static AppImageFile load(Path appImageDir) throws IOException {
 145         try {
 146             Document doc = readXml(appImageDir);
 147 
 148             XPath xPath = XPathFactory.newInstance().newXPath();
 149 
 150             String mainLauncher = xpathQueryNullable(xPath,
 151                     "/jpackage-state/main-launcher/text()", doc);
 152             if (mainLauncher == null) {
 153                 // No main launcher, this is fatal.
 154                 return new AppImageFile();
 155             }
 156 
 157             List<String> addLaunchers = new ArrayList<>();
 158 
 159             String platform = xpathQueryNullable(xPath,
 160                     "/jpackage-state/@platform", doc);
 161 
 162             String version = xpathQueryNullable(xPath,
 163                     "/jpackage-state/@version", doc);
 164 
 165             NodeList launcherNameNodes = (NodeList) xPath.evaluate(
 166                     "/jpackage-state/add-launcher/text()", doc,
 167                     XPathConstants.NODESET);
 168 
 169             for (int i = 0; i != launcherNameNodes.getLength(); i++) {
 170                 addLaunchers.add(launcherNameNodes.item(i).getNodeValue());
 171             }
 172 
 173             AppImageFile file = new AppImageFile(
 174                     mainLauncher, addLaunchers, version, platform);
 175             if (!file.isValid()) {
 176                 file = new AppImageFile();
 177             }
 178             return file;
 179         } catch (XPathExpressionException ex) {
 180             // This should never happen as XPath expressions should be correct
 181             throw new RuntimeException(ex);
 182         }
 183     }
 184 
 185     public static Document readXml(Path appImageDir) throws IOException {
 186         try {
 187             Path path = getPathInAppImage(appImageDir);
 188 
 189             DocumentBuilderFactory dbf =
 190                     DocumentBuilderFactory.newDefaultInstance();
 191             dbf.setFeature(
 192                    "http://apache.org/xml/features/nonvalidating/load-external-dtd",
 193                     false);
 194             DocumentBuilder b = dbf.newDocumentBuilder();
 195             return b.parse(Files.newInputStream(path));
 196         } catch (ParserConfigurationException | SAXException ex) {
 197             // Let caller sort this out
 198             throw new IOException(ex);
 199         }
 200     }
 201 
 202     /**
 203      * Returns list of launcher names configured for the application.
 204      * The first item in the returned list is main launcher name.
 205      * Following items in the list are names of additional launchers.
 206      */
 207     static List<String> getLauncherNames(Path appImageDir,
 208             Map<String, ? super Object> params) {
 209         List<String> launchers = new ArrayList<>();
 210         try {
 211             AppImageFile appImageInfo = AppImageFile.load(appImageDir);
 212             if (appImageInfo != null) {
 213                 launchers.add(appImageInfo.getLauncherName());
 214                 launchers.addAll(appImageInfo.getAddLauncherNames());
 215                 return launchers;
 216             }
 217         } catch (IOException ioe) {
 218             Log.verbose(ioe);
 219         }
 220 
 221         launchers.add(APP_NAME.fetchFrom(params));
 222         ADD_LAUNCHERS.fetchFrom(params).stream().map(APP_NAME::fetchFrom).forEach(
 223                 launchers::add);
 224         return launchers;
 225     }
 226 
 227     private static String xpathQueryNullable(XPath xPath, String xpathExpr,
 228             Document xml) throws XPathExpressionException {
 229         NodeList nodes = (NodeList) xPath.evaluate(xpathExpr, xml,
 230                 XPathConstants.NODESET);
 231         if (nodes != null && nodes.getLength() > 0) {
 232             return nodes.item(0).getNodeValue();
 233         }
 234         return null;
 235     }
 236 
 237     private static String getVersion() {
 238         return System.getProperty("java.version");
 239     }
 240 
 241     private static String getPlatform() {
 242         return PLATFORM_LABELS.get(Platform.getPlatform());
 243     }
 244 
 245     private boolean isValid() {
 246         if (launcherName == null || launcherName.length() == 0 ||
 247             addLauncherNames.indexOf("") != -1) {
 248             // Some launchers have empty names. This is invalid.
 249             return false;
 250         }
 251 
 252         // Add more validation.
 253 
 254         return true;
 255     }
 256 
 257 }