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