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 }