package jdk.incubator.jpackage.internal;
import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.text.MessageFormat;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static jdk.incubator.jpackage.internal.StandardBundlerParam.RESOURCE_DIR;
import jdk.incubator.jpackage.internal.resources.ResourceLocator;
final class OverridableResource {
OverridableResource(String defaultName) {
this.defaultName = defaultName;
setSourceOrder(Source.values());
}
OverridableResource setSubstitutionData(Map<String, String> v) {
if (v != null) {
substitutionData = new HashMap<>(v);
} else {
substitutionData = null;
}
return this;
}
OverridableResource setCategory(String v) {
category = v;
return this;
}
OverridableResource setResourceDir(Path v) {
resourceDir = v;
return this;
}
OverridableResource setResourceDir(File v) {
return setResourceDir(toPath(v));
}
enum Source { External, ResourceDir, DefaultResource };
OverridableResource setSourceOrder(Source... v) {
sources = Stream.of(v)
.map(source -> Map.entry(source, getHandler(source)))
.collect(Collectors.toList());
return this;
}
OverridableResource setPublicName(Path v) {
publicName = v;
return this;
}
OverridableResource setPublicName(String v) {
return setPublicName(Path.of(v));
}
OverridableResource setLogPublicName(Path v) {
logPublicName = v;
return this;
}
OverridableResource setLogPublicName(String v) {
return setLogPublicName(Path.of(v));
}
OverridableResource setExternal(Path v) {
externalPath = v;
return this;
}
OverridableResource setExternal(File v) {
return setExternal(toPath(v));
}
Source saveToStream(OutputStream dest) throws IOException {
if (dest == null) {
return sendToConsumer(null);
}
return sendToConsumer(new ResourceConsumer() {
@Override
public Path publicName() {
throw new UnsupportedOperationException();
}
@Override
public void consume(InputStream in) throws IOException {
in.transferTo(dest);
}
});
}
Source saveToFile(Path dest) throws IOException {
if (dest == null) {
return sendToConsumer(null);
}
return sendToConsumer(new ResourceConsumer() {
@Override
public Path publicName() {
return dest.getFileName();
}
@Override
public void consume(InputStream in) throws IOException {
Files.createDirectories(dest.getParent());
Files.copy(in, dest, StandardCopyOption.REPLACE_EXISTING);
}
});
}
Source saveToFile(File dest) throws IOException {
return saveToFile(toPath(dest));
}
static InputStream readDefault(String resourceName) {
return ResourceLocator.class.getResourceAsStream(resourceName);
}
static OverridableResource createResource(String defaultName,
Map<String, ? super Object> params) {
return new OverridableResource(defaultName).setResourceDir(
RESOURCE_DIR.fetchFrom(params));
}
private Source sendToConsumer(ResourceConsumer consumer) throws IOException {
for (var source: sources) {
if (source.getValue().apply(consumer)) {
return source.getKey();
}
}
return null;
}
private String getPrintableCategory() {
if (category != null) {
return String.format("[%s]", category);
}
return "";
}
private boolean useExternal(ResourceConsumer dest) throws IOException {
boolean used = externalPath != null && Files.exists(externalPath);
if (used && dest != null) {
Log.verbose(MessageFormat.format(I18N.getString(
"message.using-custom-resource-from-file"),
getPrintableCategory(),
externalPath.toAbsolutePath().normalize()));
try (InputStream in = Files.newInputStream(externalPath)) {
processResourceStream(in, dest);
}
}
return used;
}
private boolean useResourceDir(ResourceConsumer dest) throws IOException {
boolean used = false;
if (dest == null && publicName == null) {
throw new IllegalStateException();
}
final Path resourceName = Optional.ofNullable(publicName).orElseGet(
() -> dest.publicName());
if (resourceDir != null) {
final Path customResource = resourceDir.resolve(resourceName);
used = Files.exists(customResource);
if (used && dest != null) {
final Path logResourceName;
if (logPublicName != null) {
logResourceName = logPublicName.normalize();
} else {
logResourceName = resourceName.normalize();
}
Log.verbose(MessageFormat.format(I18N.getString(
"message.using-custom-resource"), getPrintableCategory(),
logResourceName));
try (InputStream in = Files.newInputStream(customResource)) {
processResourceStream(in, dest);
}
}
}
return used;
}
private boolean useDefault(ResourceConsumer dest) throws IOException {
boolean used = defaultName != null;
if (used && dest != null) {
final Path resourceName = Optional
.ofNullable(logPublicName)
.orElse(Optional
.ofNullable(publicName)
.orElseGet(() -> dest.publicName()));
Log.verbose(MessageFormat.format(
I18N.getString("message.using-default-resource"),
defaultName, getPrintableCategory(), resourceName));
try (InputStream in = readDefault(defaultName)) {
processResourceStream(in, dest);
}
}
return used;
}
private static Stream<String> substitute(Stream<String> lines,
Map<String, String> substitutionData) {
return lines.map(line -> {
String result = line;
for (var entry : substitutionData.entrySet()) {
result = result.replace(entry.getKey(), Optional.ofNullable(
entry.getValue()).orElse(""));
}
return result;
});
}
private static Path toPath(File v) {
if (v != null) {
return v.toPath();
}
return null;
}
private void processResourceStream(InputStream rawResource,
ResourceConsumer dest) throws IOException {
if (substitutionData == null) {
dest.consume(rawResource);
} else {
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(rawResource, StandardCharsets.UTF_8))) {
String data = substitute(reader.lines(), substitutionData).collect(
Collectors.joining("\n", "", "\n"));
try (InputStream in = new ByteArrayInputStream(data.getBytes(
StandardCharsets.UTF_8))) {
dest.consume(in);
}
}
}
}
private SourceHandler getHandler(Source sourceType) {
switch (sourceType) {
case DefaultResource:
return this::useDefault;
case External:
return this::useExternal;
case ResourceDir:
return this::useResourceDir;
default:
throw new IllegalArgumentException();
}
}
private Map<String, String> substitutionData;
private String category;
private Path resourceDir;
private Path publicName;
private Path logPublicName;
private Path externalPath;
private final String defaultName;
private List<Map.Entry<Source, SourceHandler>> sources;
@FunctionalInterface
private static interface SourceHandler {
public boolean apply(ResourceConsumer dest) throws IOException;
}
private static interface ResourceConsumer {
public Path publicName();
public void consume(InputStream in) throws IOException;
}
}