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.*; 28 import java.nio.charset.StandardCharsets; 29 import java.nio.file.Files; 30 import java.nio.file.Path; 31 import java.nio.file.StandardCopyOption; 32 import java.text.MessageFormat; 33 import java.util.*; 34 import java.util.stream.Collectors; 35 import java.util.stream.Stream; 36 import static jdk.incubator.jpackage.internal.StandardBundlerParam.RESOURCE_DIR; 37 import jdk.incubator.jpackage.internal.resources.ResourceLocator; 38 39 /** 40 * Resource file that may have the default value supplied by jpackage. It can be 41 * overridden by a file from resource directory set with {@code --resource-dir} 42 * jpackage parameter. 43 * 44 * Resource has default name and public name. Default name is the name of a file 45 * in {@code jdk.incubator.jpackage.internal.resources} package that provides the default 46 * value of the resource. 47 * 48 * Public name is a path relative to resource directory to a file with custom 49 * value of the resource. 50 * 51 * Use #setPublicName to set the public name. 52 * 53 * If #setPublicName was not called, name of file passed in #saveToFile function 54 * will be used as a public name. 55 * 56 * Use #setExternal to set arbitrary file as a source of resource. If non-null 57 * value was passed in #setExternal call that value will be used as a path to file 58 * to copy in the destination file passed in #saveToFile function call. 59 */ 60 final class OverridableResource { 61 62 OverridableResource(String defaultName) { 63 this.defaultName = defaultName; 64 setSourceOrder(Source.values()); 65 } 66 67 OverridableResource setSubstitutionData(Map<String, String> v) { 68 if (v != null) { 69 // Disconnect `v` 70 substitutionData = new HashMap<>(v); 71 } else { 72 substitutionData = null; 73 } 74 return this; 75 } 76 77 OverridableResource setCategory(String v) { 78 category = v; 79 return this; 80 } 81 82 OverridableResource setResourceDir(Path v) { 83 resourceDir = v; 84 return this; 85 } 86 87 OverridableResource setResourceDir(File v) { 88 return setResourceDir(toPath(v)); 89 } 90 91 enum Source { External, ResourceDir, DefaultResource }; 92 93 OverridableResource setSourceOrder(Source... v) { 94 sources = Stream.of(v) 95 .map(source -> Map.entry(source, getHandler(source))) 96 .collect(Collectors.toList()); 97 return this; 98 } 99 100 /** 101 * Set name of file to look for in resource dir. 102 * 103 * @return this 104 */ 105 OverridableResource setPublicName(Path v) { 106 publicName = v; 107 return this; 108 } 109 110 OverridableResource setPublicName(String v) { 111 return setPublicName(Path.of(v)); 112 } 113 114 /** 115 * Set name of file to look for in resource dir to put in verbose log. 116 * 117 * @return this 118 */ 119 OverridableResource setLogPublicName(Path v) { 120 logPublicName = v; 121 return this; 122 } 123 124 OverridableResource setLogPublicName(String v) { 125 return setLogPublicName(Path.of(v)); 126 } 127 128 OverridableResource setExternal(Path v) { 129 externalPath = v; 130 return this; 131 } 132 133 OverridableResource setExternal(File v) { 134 return setExternal(toPath(v)); 135 } 136 137 Source saveToFile(Path dest) throws IOException { 138 for (var source: sources) { 139 if (source.getValue().apply(dest)) { 140 return source.getKey(); 141 } 142 } 143 return null; 144 } 145 146 Source saveToFile(File dest) throws IOException { 147 return saveToFile(toPath(dest)); 148 } 149 150 static InputStream readDefault(String resourceName) { 151 return ResourceLocator.class.getResourceAsStream(resourceName); 152 } 153 154 static OverridableResource createResource(String defaultName, 155 Map<String, ? super Object> params) { 156 return new OverridableResource(defaultName).setResourceDir( 157 RESOURCE_DIR.fetchFrom(params)); 158 } 159 160 private String getPrintableCategory() { 161 if (category != null) { 162 return String.format("[%s]", category); 163 } 164 return ""; 165 } 166 167 private boolean useExternal(Path dest) throws IOException { 168 boolean used = externalPath != null && Files.exists(externalPath); 169 if (used && dest != null) { 170 Log.verbose(MessageFormat.format(I18N.getString( 171 "message.using-custom-resource-from-file"), 172 getPrintableCategory(), 173 externalPath.toAbsolutePath().normalize())); 174 175 try (InputStream in = Files.newInputStream(externalPath)) { 176 processResourceStream(in, dest); 177 } 178 } 179 return used; 180 } 181 182 private boolean useResourceDir(Path dest) throws IOException { 183 boolean used = false; 184 185 if (dest == null && publicName == null) { 186 throw new IllegalStateException(); 187 } 188 189 final Path resourceName = Optional.ofNullable(publicName).orElseGet( 190 () -> dest.getFileName()); 191 192 if (resourceDir != null) { 193 final Path customResource = resourceDir.resolve(resourceName); 194 used = Files.exists(customResource); 195 if (used && dest != null) { 196 final Path logResourceName; 197 if (logPublicName != null) { 198 logResourceName = logPublicName.normalize(); 199 } else { 200 logResourceName = resourceName.normalize(); 201 } 202 203 Log.verbose(MessageFormat.format(I18N.getString( 204 "message.using-custom-resource"), getPrintableCategory(), 205 logResourceName)); 206 207 try (InputStream in = Files.newInputStream(customResource)) { 208 processResourceStream(in, dest); 209 } 210 } 211 } 212 213 return used; 214 } 215 216 private boolean useDefault(Path dest) throws IOException { 217 boolean used = defaultName != null; 218 if (used && dest != null) { 219 final Path resourceName = Optional 220 .ofNullable(logPublicName) 221 .orElse(Optional 222 .ofNullable(publicName) 223 .orElseGet(() -> dest.getFileName())); 224 Log.verbose(MessageFormat.format( 225 I18N.getString("message.using-default-resource"), 226 defaultName, getPrintableCategory(), resourceName)); 227 228 try (InputStream in = readDefault(defaultName)) { 229 processResourceStream(in, dest); 230 } 231 } 232 return used; 233 } 234 235 private static List<String> substitute(Stream<String> lines, 236 Map<String, String> substitutionData) { 237 return lines.map(line -> { 238 String result = line; 239 for (var entry : substitutionData.entrySet()) { 240 result = result.replace(entry.getKey(), Optional.ofNullable( 241 entry.getValue()).orElse("")); 242 } 243 return result; 244 }).collect(Collectors.toList()); 245 } 246 247 private static Path toPath(File v) { 248 if (v != null) { 249 return v.toPath(); 250 } 251 return null; 252 } 253 254 private void processResourceStream(InputStream rawResource, Path dest) 255 throws IOException { 256 if (substitutionData == null) { 257 Files.createDirectories(dest.getParent()); 258 Files.copy(rawResource, dest, StandardCopyOption.REPLACE_EXISTING); 259 } else { 260 // Utf8 in and out 261 try (BufferedReader reader = new BufferedReader( 262 new InputStreamReader(rawResource, StandardCharsets.UTF_8))) { 263 Files.createDirectories(dest.getParent()); 264 Files.write(dest, substitute(reader.lines(), substitutionData)); 265 } 266 } 267 } 268 269 private SourceHandler getHandler(Source sourceType) { 270 switch (sourceType) { 271 case DefaultResource: 272 return this::useDefault; 273 274 case External: 275 return this::useExternal; 276 277 case ResourceDir: 278 return this::useResourceDir; 279 280 default: 281 throw new IllegalArgumentException(); 282 } 283 } 284 285 private Map<String, String> substitutionData; 286 private String category; 287 private Path resourceDir; 288 private Path publicName; 289 private Path logPublicName; 290 private Path externalPath; 291 private final String defaultName; 292 private List<Map.Entry<Source, SourceHandler>> sources; 293 294 @FunctionalInterface 295 static interface SourceHandler { 296 public boolean apply(Path dest) throws IOException; 297 } 298 }