
org.update4j.FileMetadata Maven / Gradle / Ivy
Show all versions of update4j Show documentation
/*
* Copyright 2018 Mordechai Meisels
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package org.update4j;
import java.io.IOException;
import java.net.URI;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.security.PrivateKey;
import java.util.ArrayList;
import java.util.Base64;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
import java.util.function.Consumer;
import java.util.function.Predicate;
import java.util.stream.Stream;
import org.update4j.mapper.ConfigMapper;
import org.update4j.mapper.FileMapper;
import org.update4j.service.UpdateHandler;
import org.update4j.util.FileUtils;
import org.update4j.util.PropertyManager;
/**
* This class represents a managed file (—in sense of updating and
* dynamically loading onto a JVM instance upon launch) in this framework. It
* corresponds with the {@code } XML element.
*
*
* Some metadata is required for updating only, some are required for launch
* only, and some are for both. The documentation will try to point that out for
* each field.
*
*
* An instance of this class cannot be created directly, only
* {@link Configuration.Builder} and {@link Configuration#parse(ConfigMapper)}
* can. For both approaches you use a special interim object: The builder takes
* a {@link FileMetadata.Reference} object in the {@code file()} method —
* created by either {@link FileMetadata#readFrom(Path)} for single files, or
* {@link FileMetadata#streamDirectory(Path)} for a complete directory. The
* {@code parse()} method works with the {@link ConfigMapper} class that lists
* files with {@link FileMapper}s.
*
*
* An instance of this class is immutable and thus thread-safe.
*
* @author Mordechai Meisels
*
*/
public class FileMetadata {
private final URI uri;
private final Path path;
private Path normalizedPath;
private final OS os;
private final long checksum;
private final long size;
private final boolean classpath;
private final boolean modulepath;
private final String comment;
private final boolean ignoreBootConflict;
private final String signature;
private final List addExports;
private final List addOpens;
private final List addReads;
private FileMetadata(URI uri, Path path, OS os, long checksum, long size, boolean classpath, boolean modulepath,
String comment, boolean ignoreBootConflict, String signature, List addExports,
List addOpens, List addReads) {
this.uri = uri;
// parsing properties might fail sometimes when not on current os, so let it through
if (os == null || os == OS.CURRENT) {
Objects.requireNonNull(uri, "uri");
if (!uri.isAbsolute()) {
throw new IllegalArgumentException("Absolute uri required: " + uri);
}
}
this.path = path;
if (os == null || os == OS.CURRENT) {
Objects.requireNonNull(path, "path");
if (!path.isAbsolute()) {
throw new IllegalArgumentException("Absolute path required: " + path);
}
}
this.os = os;
if (checksum < 0)
throw new IllegalArgumentException("Negative checksum: " + checksum);
this.checksum = checksum;
if (size < 0)
throw new IllegalArgumentException("Negative file size: " + size);
this.size = size;
this.classpath = classpath;
this.modulepath = modulepath;
this.comment = comment;
this.ignoreBootConflict = ignoreBootConflict;
this.signature = signature;
this.addExports = Collections.unmodifiableList(new ArrayList<>(addExports));
this.addOpens = Collections.unmodifiableList(new ArrayList<>(addOpens));
this.addReads = Collections.unmodifiableList(new ArrayList<>(addReads));
}
/**
* Returns the download URI for this file. This might be directly expressed in
* the {@code uri} attribute as an absolute uri, relative to the base uri, or
* — if missing — inferred from the {@code path} attribute.
*
*
* When inferring from the path it will use the complete path structure if
* — and only if — the path is relative to the base path. Otherwise
* it will only use the last part (i.e. the "filename").
*
*
*
* If this file is marked for a foreign OS and the URI has a foreign property in
* the file (a property marked for a different {@code os}), it will return
* {@code null}.
*
*
* This field is only used for updating.
*
* @return The download URI for this file.
*/
public URI getUri() {
return uri;
}
/**
* Returns the local path for this file. This might be directly expressed in the
* {@code path} attribute as an absolute path, relative to the base path, or
* — if missing — inferred from the {@code uri} attribute.
*
*
* When inferring from the uri it will use the complete path structure if
* — and only if — the uri is relative to the base uri. Otherwise it
* will only use the last part (i.e. the "filename").
*
*
* If this file is marked for a foreign OS and the path has a foreign property
* in the file (a property marked for a different {@code os}), it will return
* {@code null}.
*
*
* This field is used for both updating and launching.
*
* @return The local path for this file.
*/
public Path getPath() {
return path;
}
Path getNormalizedPath() {
if(getPath() == null)
return null;
if(normalizedPath == null)
normalizedPath = path.normalize();
return normalizedPath;
}
/**
* Returns the operating system expressed in the {@code os} attribute, or
* {@code null} if non.
*
*
* This field is used for both updating and launching.
*
* @return The operating system expressed in the {@code os} attribute, or
* {@code null} if non.
*/
public OS getOs() {
return os;
}
/**
* Returns the Adler32 checksum of this file. Used to check if an update is
* needed and to validate the file post-download.
*
*
* This field is only used for updating.
*
* @return The Adler32 checksum of this file.
*/
public long getChecksum() {
return checksum;
}
/**
* Returns the file size. Used to check if an update is needed, validate the
* file post-download, and to calculate proper download deltas.
*
*
* This field is only used for updating.
*
* @return The file size.
*/
public long getSize() {
return size;
}
/**
* Returns if this file is marked to be loaded on the dynamic classpath. Files
* in the bootstrap and non-jar files should generally be marked {@code false}.
*
*
* This field is only used for launching.
*
* @return If this file is marked to be loaded on the dynamic classpath.
*/
public boolean isClasspath() {
return classpath;
}
/**
* Returns if this file is marked to be loaded on the dynamic modulepath. Files
* in the bootstrap should generally be marked {@code false}. Non-jar files must
* not mark this {@code true} or the JVM will fail.
*
*
* This field is only used for launching.
*
* @return If this file is marked to be loaded on the dynamic classpath.
*/
public boolean isModulepath() {
return modulepath;
}
/**
* Returns a string from the {@code comment} attribute, or {@code null} if
* missing.
*
*
* This has no effect on the framework and can be used for just anything. For
* instance: you might mark this file with {@code requiresRestart} to notify the
* update handler to restart the application if this file was part of the
* update. Or you might put an authentication key which might then be used in
* the download by overriding
* {@link UpdateHandler#openDownloadStream(FileMetadata)}.
*
*
* @return A string from the {@code comment} attribute, or {@code null} if
* missing.
*/
public String getComment() {
return comment;
}
/**
* Returns whether this file was marked to ignore a boot conflict check.
*
*
* The boot conflict check is a safety measure put in place to prevent breaking
* your remote applications, and then being impossible to fix remotely just by
* pushing a new release.
* If you push a file that then gets loaded onto the boot modulepath
* (by placing it in a location that is searched by the modulepath) and that
* file ends up being a invalid module (as split package, duplicate module name
* or just not a zip file), the JVM will complain and resist to start up. Since
* we can't start the application anymore, there is no way to fix this other
* than reinstalling the application.
*
*
* This file check is done for each and every file with the {@code .jar} file
* extension, even if the file was explicitly marked with the {@code modulepath}
* (meaning it is only loaded on the dynamic modulepath).
*
*
* In cases where the file is not visible to the boot modulepath (by carefully
* placing it in the right directory) and you want to circumvent this check you
* can mark this {@code true}.
*
*
* This field is only used for updating.
*
* @return If this file is marked to ignore a boot conflict check.
*/
public boolean isIgnoreBootConflict() {
return ignoreBootConflict;
}
/**
* Returns the Base64 encoded file signature.
*
*
* This field is only used for updating.
*
* @return The Base64 encoded file signature.
*/
public String getSignature() {
return signature;
}
/**
* Returns an unmodifiable list of packages that should be exported to a module
* despite not being defined so in the {@code module-info.class} file.
*
*
* This is ignored if {@link #isModulepath()} returns {@code false}.
*
*
* This field is only used for launching.
*
* @return A list of packages to be exported to other modules.
*/
public List getAddExports() {
return addExports;
}
/**
* Returns an unmodifiable list of packages that should be opened to a module
* despite not being defined so in the {@code module-info.class} file.
*
*
* This is ignored if {@link #isModulepath()} returns {@code false}.
*
*
* This field is only used for launching.
*
* @return A list of packages to be opened to other modules.
*/
public List getAddOpens() {
return addOpens;
}
/**
* Returns an unmodifiable list modules this module should read despite not
* being defined so in the {@code module-info.class} file.
*
*
* This is ignored if {@link #isModulepath()} returns {@code false}.
*
*
* This field is only used for launching.
*
* @return A list of modules this module should read.
*/
public List getAddReads() {
return addReads;
}
/**
* Checks if this file is out of date and requires an update.
*
* @return If this file requires an update.
*
* @throws IOException
* If any exception arises while reading the file content.
*/
public boolean requiresUpdate() throws IOException {
if (getOs() != null && getOs() != OS.CURRENT)
return false;
return Files.notExists(getPath()) || Files.size(getPath()) != getSize()
|| FileUtils.getChecksum(getPath()) != getChecksum();
}
/**
* Construct a {@link Reference} of the file at the provided location and can be
* used in the Builder API.
*
*
* This should point to a real file on the filesystem and cannot contain
* placeholders.
*
* @param source
* The path of the real file to which to refer in the builder.
*
* @return A {@code Reference} to a file to be used in the Builder API.
*/
public static Reference readFrom(Path source) {
return new Reference(source);
}
/**
* Construct a {@link Reference} of the file at the provided location and can be
* used in the Builder API.
*
*
* This should point to a real file on the filesystem and cannot contain
* placeholders.
*
* @param source
* The path of the real file to which to refer in the builder.
*
* @return A {@code Reference} to a file to be used in the Builder API.
*/
public static Reference readFrom(String source) {
return readFrom(Paths.get(source));
}
/**
* Construct a stream of file {@link Reference}s from the provided directory.
* You can then customize individual files by using
* {@link Stream#peek(Consumer)}, or filter out files with
* {@link Stream#filter(Predicate)}. It will only contain files and symlinks,
* not directories.
*
*
* For convenience, this method also presets the {@code path()} to the file's
* source path relative to to streamed directory. So for the directory
* structure {@code /home/a/b/c.jar}, streaming {@code /home/a} would set the
* path to {@code b/c.jar}.
* If you wish to infer the path from the URI (as described in
* {@link FileMetadata.Reference#path(String)}) you must nullify it by calling
* {@code path((String)null)}.
*
*
* This should point to a real directory on the filesystem and cannot contain
* placeholders.
*
* @param source
* The path of the real directory to stream.
*
* @return A {@code Stream} of files to be used in the Builder API.
*/
public static Stream streamDirectory(Path dir) {
try {
return Files.walk(dir)
.filter(p -> Files.isRegularFile(p))
.map(FileMetadata::readFrom)
.peek(fm -> fm.path(dir.relativize(fm.getSource())));
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Construct a stream of file {@link Reference}s from the provided directory.
* You can then customize individual files by using
* {@link Stream#peek(Consumer)}, or filter out files with
* {@link Stream#filter(Predicate)}. It will only contain files and symlinks,
* not directories.
*
*
* For convenience, this method also presets the {@code path()} to the file's
* source path relative to to streamed directory. So for the directory
* structure {@code /home/a/b/c.jar}, streaming {@code /home/a} would set the
* path to {@code b/c.jar}.
* If you wish to infer the path from the URI (as described in
* {@link FileMetadata.Reference#path(String)}) you must nullify it by calling
* {@code path((String)null)}.
*
*
* This should point to a real directory on the filesystem and cannot contain
* placeholders.
*
* @param source
* The path of the real directory to stream.
*
* @return A {@code Stream} of files to be used in the Builder API.
*/
public static Stream streamDirectory(String dir) {
return streamDirectory(Paths.get(dir));
}
/**
* A reference to a file can be used by the Builder API to read its metadata.
* You can construct a reference by using either
* {@link FileMetadata#readFrom(Path)} or
* {@link FileMetadata#streamDirectory(Path)}.
*
* @author Mordechai Meisels
*
*/
public static class Reference {
private Path source;
private String path;
private String uri;
private OS os;
private Boolean classpath;
private Boolean modulepath;
private String comment;
private Boolean ignoreBootConflict;
private List addExports;
private List addOpens;
private List addReads;
private PlaceholderMatchType matcher;
private Reference(Path source) {
this.source = source;
addExports = new ArrayList<>();
addOpens = new ArrayList<>();
addReads = new ArrayList<>();
}
/**
* Returns the real file location that this instance refers to.
*
* @return The real file location that this instance refers to.
*/
public Path getSource() {
return source;
}
/**
* Set the download URI for this file. This might be an absolute uri, relative
* to the base uri, or — if missing — inferred from the
* {@code path}.
*
*
* When inferring from the path it will use the complete path structure if
* — and only if — the path is relative to the base path. Otherwise
* it will only use the last part (i.e. the "filename").
*
* @return This instance for chaining.
*/
public Reference uri(URI uri) {
return uri(uri == null ? null : uri.toString());
}
/**
* Set the download URI for this file. This might be an absolute uri, relative
* to the base uri, or — if missing — inferred from the
* {@code path}.
*
*
* When inferring from the path it will use the complete path structure if
* — and only if — the path is relative to the base path. Otherwise
* it will only use the last part (i.e. the "filename").
*
*
* This field may contain placeholders.
*
* @return This instance for chaining.
*/
public Reference uri(String uri) {
this.uri = uri;
return this;
}
/**
* Returns the URI passed in {@link #uri(String)} or {@code null} if non.
*
* @return The URI passed in {@link #uri(String)} or {@code null}.
*/
public String getUri() {
return uri;
}
/**
* Sets the local path for this file. This might be an absolute path, relative
* to the base path, or — if missing — inferred from the {@code uri}
* attribute, if both are missing, the source returned by {@link #getSource()}.
*
*
* When inferring from the uri it will use the complete path structure if
* — and only if — the uri is relative to the base uri. Otherwise it
* will only use the last part (i.e. the "filename").
*
*
* @return This instance for chaining.
*/
public Reference path(Path path) {
return path(path == null ? null : path.toString());
}
/**
* Sets the local path for this file. This might be an absolute path, relative
* to the base path, or — if missing — inferred from the {@code uri}
* attribute, if both are missing, the source returned by {@link #getSource()}.
*
*
* When inferring from the uri it will use the complete path structure if
* — and only if — the uri is relative to the base uri. Otherwise it
* will only use the last part (i.e. the "filename").
*
*
* @return This instance for chaining.
*/
public Reference path(String path) {
this.path = path;
return this;
}
/**
* Returns the path passed in {@link #path(String)} or {@code null} if non.
*
* @return The path passed in {@link #path(String)} or {@code null}.
*/
String getPath() {
return path;
}
/**
* Sets the os of this file to exclude it from other operating systems when
* updating and launching.
*
* @param os
* The operating system to associate this file with.
* @return This instance for chaining.
*/
public Reference os(OS os) {
this.os = os;
return this;
}
/**
* Sets the os by parsing the filename. The os is detected if it matches this
* pattern:
*
*
* filename-os.extension
*
*
* where {@code os} can be {@code win}, {@code mac} or {@code linux}.
*
*
* Examples include:
*
*
* appicon-win.ico
* appicon-mac.icns
* appicon-linux.png
*
* javafx-base-11.0.1-win.jar
* javafx-base-11.0.1-mac.jar
* javafx-base-11.0.1-linux.jar
*
*
*
* If a match is not found, the old value will not be changed.
*
* @return This instance for chaining.
*/
public Reference osFromFilename() {
OS os = FileUtils.fromFilename(source.toString());
if (os != null)
os(os);
return this;
}
/**
* Returns the os passed in {@link #os(OS)} or {@code null} if non.
*
* @return The os passed in {@link #os(OS)} or {@code null}.
*/
public OS getOs() {
return os;
}
public Reference classpath(boolean cp) {
this.classpath = cp;
return this;
}
public Reference classpath() {
return classpath(true);
}
public boolean isClasspath() {
return Boolean.TRUE.equals(classpath);
}
public Reference modulepath(boolean mp) {
this.modulepath = mp;
return this;
}
public Reference modulepath() {
return modulepath(true);
}
public boolean isModulepath() {
return Boolean.TRUE.equals(modulepath);
}
private boolean isFinalModulepath() throws IOException {
if (!isModulepath()) {
return false;
}
// If not a zip file, completely ignore modulepath
return FileUtils.isZipFile(getSource());
}
public Reference comment(String c) {
comment = c;
return this;
}
public String getComment() {
return comment;
}
public Reference ignoreBootConflict(boolean b) {
ignoreBootConflict = b;
return this;
}
public Reference ignoreBootConflict() {
return ignoreBootConflict(true);
}
public boolean isIgnoreBootConflict() {
return Boolean.TRUE.equals(ignoreBootConflict);
}
public Reference exports(String pkg, String targetModule) {
addExports.add(new AddPackage(Objects.requireNonNull(pkg), Objects.requireNonNull(targetModule)));
return this;
}
public Reference exports(Collection exports) {
addExports.addAll(exports);
return this;
}
public List getAddExports() {
return addExports;
}
public Reference opens(String pkg, String targetModule) {
addOpens.add(new AddPackage(pkg, targetModule));
return this;
}
public Reference opens(Collection opens) {
addOpens.addAll(opens);
return this;
}
public List getAddOpens() {
return addOpens;
}
public Reference reads(String module) {
addReads.add(module);
return this;
}
public Reference reads(Collection reads) {
addReads.addAll(reads);
return this;
}
public List getAddReads() {
return addReads;
}
public Reference matchAndReplace(PlaceholderMatchType matcher) {
this.matcher = matcher;
return this;
}
public PlaceholderMatchType getMatchType() {
return matcher;
}
public long getSize() throws IOException {
return Files.size(source);
}
public long getChecksum() throws IOException {
return FileUtils.getChecksum(source);
}
public byte[] getSignature(PrivateKey key) throws IOException {
if (key == null)
return null;
return FileUtils.sign(source, key);
}
FileMapper getFileMapper(PropertyManager pm, String baseUri, String basePath, PlaceholderMatchType matchType,
PrivateKey key) {
try {
String path = getPath();
if (uri == null && getPath() == null) {
path = source.toString();
}
if (uri != null) {
uri = uri.replace("\\", "/");
}
if (path != null) {
path = path.replace("\\", "/");
}
if (uri != null && uri.equals(path)) {
uri = null;
}
PlaceholderMatchType matcher = getMatchType();
if (matcher == null) {
matcher = matchType;
}
FileMapper mapper = new FileMapper();
mapper.uri = pm.implyPlaceholders(getUri(), matcher, true);
mapper.path = pm.implyPlaceholders(path, matcher, true);
// Changes behavior of cross-resolution, so watch out
String phBaseUri = pm.implyPlaceholders(baseUri, matcher, true);
if (mapper.uri != null && phBaseUri != null && mapper.uri.startsWith(phBaseUri)) {
mapper.uri = mapper.uri.substring(phBaseUri.length());
}
String phBasePath = pm.implyPlaceholders(basePath, matcher, true);
if (mapper.path != null && phBasePath != null && mapper.path.startsWith(phBasePath)) {
mapper.path = mapper.path.substring(phBasePath.length());
}
mapper.os = getOs();
mapper.size = getSize();
mapper.checksum = Long.toHexString(getChecksum());
mapper.classpath = isClasspath();
mapper.modulepath = isFinalModulepath();
mapper.ignoreBootConflict = isIgnoreBootConflict();
byte[] sig = getSignature(key);
if (sig != null)
mapper.signature = Base64.getEncoder().encodeToString(sig);
mapper.comment = pm.implyPlaceholders(getComment(), matcher, false);
mapper.addExports.addAll(getAddExports());
mapper.addOpens.addAll(getAddOpens());
mapper.addReads.addAll(getAddReads());
return mapper;
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
static Builder builder() {
return new Builder();
}
static class Builder {
private URI baseUri;
private Path basePath;
private URI uri;
private Path path;
private OS os;
private long checksum;
private long size;
private boolean classpath;
private boolean modulepath;
private String comment;
private boolean ignoreBootConflict;
private String signature;
private List addExports;
private List addOpens;
private List addReads;
private Builder() {
addExports = new ArrayList<>();
addOpens = new ArrayList<>();
addReads = new ArrayList<>();
}
Builder baseUri(URI uri) {
this.baseUri = uri;
return this;
}
Builder basePath(Path path) {
this.basePath = path;
return this;
}
Builder uri(URI uri) {
this.uri = uri;
return this;
}
Builder path(Path path) {
this.path = path;
return this;
}
Builder os(OS os) {
this.os = os;
return this;
}
Builder checksum(long checksum) {
this.checksum = checksum;
return this;
}
Builder checksum(String checksum) {
return checksum(Long.parseLong(checksum, 16));
}
Builder size(long size) {
this.size = size;
return this;
}
Builder classpath(boolean cp) {
this.classpath = cp;
return this;
}
Builder modulepath(boolean mp) {
this.modulepath = mp;
return this;
}
Builder ignoreBootConflict(boolean b) {
this.ignoreBootConflict = b;
return this;
}
Builder comment(String comment) {
this.comment = comment;
return this;
}
Builder signature(String signature) {
this.signature = signature;
return this;
}
private void validateAddReads(List list) {
for (String read : list) {
Objects.requireNonNull(read);
if (read.isEmpty()) {
throw new IllegalArgumentException("Missing module name.");
}
}
}
private void validateAddPackages(List list) {
for (AddPackage add : list) {
Objects.requireNonNull(add);
}
}
Builder exports(List exports) {
validateAddPackages(exports);
addExports.addAll(exports);
return this;
}
Builder opens(List opens) {
validateAddPackages(opens);
addOpens.addAll(opens);
return this;
}
Builder reads(List reads) {
validateAddReads(reads);
addReads.addAll(reads);
return this;
}
FileMetadata build() {
if (path == null && uri != null) {
path(FileUtils.fromUri(uri));
}
if (uri == null && path != null) {
uri(FileUtils.fromPath(path));
}
// relativization gets messed up if relative path has a leading slash
if (uri != null && !uri.isAbsolute() && uri.getPath().startsWith("/")) {
uri = URI.create("/").relativize(uri);
}
if (path != null && !path.isAbsolute() && path.startsWith("/")) {
path = Paths.get("/").relativize(path);
}
if (baseUri != null && uri != null) {
this.uri = baseUri.resolve(uri);
}
if (basePath != null && path != null) {
this.path = basePath.resolve(path);
}
return new FileMetadata(uri, path, os, checksum, size, classpath, modulepath, comment, ignoreBootConflict,
signature, addExports, addOpens, addReads);
}
}
}