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 }