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 }