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.FileInputStream;
  28 import java.io.IOException;
  29 import java.nio.file.Path;
  30 import java.util.List;
  31 import java.util.ArrayList;
  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.*;
  45 
  46 public class AppImageFile {
  47 
  48     // These values will be loaded from AppImage xml file.
  49     private final String creatorVersion;
  50     private final String creatorPlatform;
  51     private final String launcherName;
  52     private final List<String> addLauncherNames;
  53 
  54     private final static String FILENAME = ".jpackage.xml";
  55 
  56     private final static Map<Platform, String> PLATFORM_LABELS = Map.of(
  57             Platform.LINUX, "linux", Platform.WINDOWS, "windows", Platform.MAC,
  58             "macOS");
  59 
  60 
  61     private AppImageFile() {
  62         this(null, null, null, null);
  63     }
  64 
  65     private AppImageFile(String launcherName, List<String> addLauncherNames,
  66             String creatorVersion, String creatorPlatform) {
  67         this.launcherName = launcherName;
  68         this.addLauncherNames = addLauncherNames;
  69         this.creatorVersion = creatorVersion;
  70         this.creatorPlatform = creatorPlatform;
  71     }
  72 
  73     /**
  74      * Returns list of additional launchers configured for the application.
  75      * Each item in the list is not null or empty string.
  76      * Returns empty list for application without additional launchers.
  77      */
  78     List<String> getAddLauncherNames() {
  79         return addLauncherNames;
  80     }
  81 
  82     /**
  83      * Returns main application launcher name. Never returns null or empty value.
  84      */
  85     String getLauncherName() {
  86         return launcherName;
  87     }
  88 
  89     void verifyCompatible() throws ConfigException {
  90         // Just do nothing for now.
  91     }
  92 
  93     /**
  94      * Returns path to application image info file.
  95      * @param appImageDir - path to application image
  96      */
  97     public static Path getPathInAppImage(Path appImageDir) {
  98         return appImageDir.resolve(FILENAME);
  99     }
 100 
 101     /**
 102      * Saves file with application image info in application image.
 103      * @param appImageDir - path to application image
 104      * @throws IOException
 105      */
 106     static void save(Path appImageDir, Map<String, Object> params)
 107             throws IOException {
 108         IOUtils.createXml(getPathInAppImage(appImageDir), xml -> {
 109             xml.writeStartElement("jpackage-state");
 110             xml.writeAttribute("version", getVersion());
 111             xml.writeAttribute("platform", getPlatform());
 112 
 113             xml.writeStartElement("app-version");
 114             xml.writeCharacters(VERSION.fetchFrom(params));
 115             xml.writeEndElement();
 116 
 117             xml.writeStartElement("main-launcher");
 118             xml.writeCharacters(APP_NAME.fetchFrom(params));
 119             xml.writeEndElement();
 120 
 121             List<Map<String, ? super Object>> addLaunchers =
 122                 ADD_LAUNCHERS.fetchFrom(params);
 123 
 124             for (int i = 0; i < addLaunchers.size(); i++) {
 125                 Map<String, ? super Object> sl = addLaunchers.get(i);
 126                 xml.writeStartElement("add-launcher");
 127                 xml.writeCharacters(APP_NAME.fetchFrom(sl));
 128                 xml.writeEndElement();
 129             }
 130         });
 131     }
 132 
 133     /**
 134      * Loads application image info from application image.
 135      * @param appImageDir - path to application image
 136      * @return valid info about application image or null
 137      * @throws IOException
 138      */
 139     static AppImageFile load(Path appImageDir) throws IOException {
 140         try {
 141             Document doc = readXml(appImageDir);
 142 
 143             XPath xPath = XPathFactory.newInstance().newXPath();
 144 
 145             String mainLauncher = xpathQueryNullable(xPath,
 146                     "/jpackage-state/main-launcher/text()", doc);
 147             if (mainLauncher == null) {
 148                 // No main launcher, this is fatal.
 149                 return new AppImageFile();
 150             }
 151 
 152             List<String> addLaunchers = new ArrayList<String>();
 153 
 154             String platform = xpathQueryNullable(xPath,
 155                     "/jpackage-state/@platform", doc);
 156 
 157             String version = xpathQueryNullable(xPath,
 158                     "/jpackage-state/@version", doc);
 159 
 160             NodeList launcherNameNodes = (NodeList) xPath.evaluate(
 161                     "/jpackage-state/add-launcher/text()", doc,
 162                     XPathConstants.NODESET);
 163 
 164             for (int i = 0; i != launcherNameNodes.getLength(); i++) {
 165                 addLaunchers.add(launcherNameNodes.item(i).getNodeValue());
 166             }
 167 
 168             AppImageFile file = new AppImageFile(
 169                     mainLauncher, addLaunchers, version, platform);
 170             if (!file.isValid()) {
 171                 file = new AppImageFile();
 172             }
 173             return file;
 174         } catch (XPathExpressionException ex) {
 175             // This should never happen as XPath expressions should be correct
 176             throw new RuntimeException(ex);
 177         }
 178     }
 179 
 180     public static Document readXml(Path appImageDir) throws IOException {
 181         try {
 182             Path path = getPathInAppImage(appImageDir);
 183 
 184             DocumentBuilderFactory dbf =
 185                     DocumentBuilderFactory.newDefaultInstance();
 186             dbf.setFeature(
 187                    "http://apache.org/xml/features/nonvalidating/load-external-dtd",
 188                     false);
 189             DocumentBuilder b = dbf.newDocumentBuilder();
 190             return b.parse(new FileInputStream(path.toFile()));
 191         } catch (ParserConfigurationException | SAXException ex) {
 192             // Let caller sort this out
 193             throw new IOException(ex);
 194         }
 195     }
 196 
 197     /**
 198      * Returns list of launcher names configured for the application.
 199      * The first item in the returned list is main launcher name.
 200      * Following items in the list are names of additional launchers.
 201      */
 202     static List<String> getLauncherNames(Path appImageDir,
 203             Map<String, ? super Object> params) {
 204         List<String> launchers = new ArrayList<>();
 205         try {
 206             AppImageFile appImageInfo = AppImageFile.load(appImageDir);
 207             if (appImageInfo != null) {
 208                 launchers.add(appImageInfo.getLauncherName());
 209                 launchers.addAll(appImageInfo.getAddLauncherNames());
 210                 return launchers;
 211             }
 212         } catch (IOException ioe) {
 213             Log.verbose(ioe);
 214         }
 215 
 216         launchers.add(APP_NAME.fetchFrom(params));
 217         ADD_LAUNCHERS.fetchFrom(params).stream().map(APP_NAME::fetchFrom).forEach(
 218                 launchers::add);
 219         return launchers;
 220     }
 221 
 222     private static String xpathQueryNullable(XPath xPath, String xpathExpr,
 223             Document xml) throws XPathExpressionException {
 224         NodeList nodes = (NodeList) xPath.evaluate(xpathExpr, xml,
 225                 XPathConstants.NODESET);
 226         if (nodes != null && nodes.getLength() > 0) {
 227             return nodes.item(0).getNodeValue();
 228         }
 229         return null;
 230     }
 231 
 232     private static String getVersion() {
 233         return System.getProperty("java.version");
 234     }
 235 
 236     private static String getPlatform() {
 237         return PLATFORM_LABELS.get(Platform.getPlatform());
 238     }
 239 
 240     private boolean isValid() {
 241         if (launcherName == null || launcherName.length() == 0 ||
 242             addLauncherNames.indexOf("") != -1) {
 243             // Some launchers have empty names. This is invalid.
 244             return false;
 245         }
 246 
 247         // Add more validation.
 248 
 249         return true;
 250     }
 251 
 252 }