1 /*
   2  * Copyright (c) 2019, 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("main-launcher");
 114             xml.writeCharacters(APP_NAME.fetchFrom(params));
 115             xml.writeEndElement();
 116 
 117             List<Map<String, ? super Object>> addLaunchers =
 118                 ADD_LAUNCHERS.fetchFrom(params);
 119 
 120             for (int i = 0; i < addLaunchers.size(); i++) {
 121                 Map<String, ? super Object> sl = addLaunchers.get(i);
 122                 xml.writeStartElement("add-launcher");
 123                 xml.writeCharacters(APP_NAME.fetchFrom(sl));
 124                 xml.writeEndElement();
 125             }
 126         });
 127     }
 128 
 129     /**
 130      * Loads application image info from application image.
 131      * @param appImageDir - path to application image
 132      * @return valid info about application image or null
 133      * @throws IOException
 134      */
 135     static AppImageFile load(Path appImageDir) throws IOException {
 136         try {
 137             Path path = getPathInAppImage(appImageDir);
 138             DocumentBuilderFactory dbf =
 139                     DocumentBuilderFactory.newDefaultInstance();
 140             dbf.setFeature(
 141                    "http://apache.org/xml/features/nonvalidating/load-external-dtd",
 142                     false);
 143             DocumentBuilder b = dbf.newDocumentBuilder();
 144             Document doc = b.parse(new FileInputStream(path.toFile()));
 145 
 146             XPath xPath = XPathFactory.newInstance().newXPath();
 147 
 148             String mainLauncher = xpathQueryNullable(xPath,
 149                     "/jpackage-state/main-launcher/text()", doc);
 150             if (mainLauncher == null) {
 151                 // No main launcher, this is fatal.
 152                 return new AppImageFile();
 153             }
 154 
 155             List<String> addLaunchers = new ArrayList<String>();
 156 
 157             String platform = xpathQueryNullable(xPath,
 158                     "/jpackage-state/@platform", doc);
 159 
 160             String version = xpathQueryNullable(xPath,
 161                     "/jpackage-state/@version", doc);
 162 
 163             NodeList launcherNameNodes = (NodeList) xPath.evaluate(
 164                     "/jpackage-state/add-launcher/text()", doc,
 165                     XPathConstants.NODESET);
 166 
 167             for (int i = 0; i != launcherNameNodes.getLength(); i++) {
 168                 addLaunchers.add(launcherNameNodes.item(i).getNodeValue());
 169             }
 170 
 171             AppImageFile file = new AppImageFile(
 172                     mainLauncher, addLaunchers, version, platform);
 173             if (!file.isValid()) {
 174                 file = new AppImageFile();
 175             }
 176             return file;
 177         } catch (ParserConfigurationException | SAXException ex) {
 178             // Let caller sort this out
 179             throw new IOException(ex);
 180         } catch (XPathExpressionException ex) {
 181             // This should never happen as XPath expressions should be correct
 182             throw new RuntimeException(ex);
 183         }
 184     }
 185 
 186     /**
 187      * Returns list of launcher names configured for the application.
 188      * The first item in the returned list is main launcher name.
 189      * Following items in the list are names of additional launchers.
 190      */
 191     static List<String> getLauncherNames(Path appImageDir,
 192             Map<String, ? super Object> params) {
 193         List<String> launchers = new ArrayList<>();
 194         try {
 195             AppImageFile appImageInfo = AppImageFile.load(appImageDir);
 196             if (appImageInfo != null) {
 197                 launchers.add(appImageInfo.getLauncherName());
 198                 launchers.addAll(appImageInfo.getAddLauncherNames());
 199                 return launchers;
 200             }
 201         } catch (IOException ioe) {
 202             Log.verbose(ioe);
 203         }
 204 
 205         launchers.add(APP_NAME.fetchFrom(params));
 206         ADD_LAUNCHERS.fetchFrom(params).stream().map(APP_NAME::fetchFrom).forEach(
 207                 launchers::add);
 208         return launchers;
 209     }
 210 
 211     private static String xpathQueryNullable(XPath xPath, String xpathExpr,
 212             Document xml) throws XPathExpressionException {
 213         NodeList nodes = (NodeList) xPath.evaluate(xpathExpr, xml,
 214                 XPathConstants.NODESET);
 215         if (nodes != null && nodes.getLength() > 0) {
 216             return nodes.item(0).getNodeValue();
 217         }
 218         return null;
 219     }
 220 
 221     private static String getVersion() {
 222         return System.getProperty("java.version");
 223     }
 224 
 225     private static String getPlatform() {
 226         return PLATFORM_LABELS.get(Platform.getPlatform());
 227     }
 228 
 229     private boolean isValid() {
 230         if (launcherName == null || launcherName.length() == 0 ||
 231             addLauncherNames.indexOf("") != -1) {
 232             // Some launchers have empty names. This is invalid.
 233             return false;
 234         }
 235 
 236         // Add more validation.
 237 
 238         return true;
 239     }
 240 
 241 }