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