/*
* Capsule
* Copyright (c) 2014-2015, Parallel Universe Software Co. All rights reserved.
*
* This program and the accompanying materials are licensed under the terms
* of the Eclipse Public License v1.0, available at
* http://www.eclipse.org/legal/epl-v10.html
*/
package co.paralleluniverse.capsule;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.jar.Attributes;
import java.util.jar.JarEntry;
import java.util.jar.JarInputStream;
import java.util.jar.JarOutputStream;
import java.util.jar.Manifest;
import java.util.jar.Pack200;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import static java.nio.charset.StandardCharsets.UTF_8;
import java.util.Collection;
import java.util.regex.Pattern;
import java.util.zip.ZipException;
/**
* A JAR file that can be easily modified.
* This class is not thread-safe.
*/
public class Jar {
private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF"; // java.util.jar.JarFile.MANIFEST_NAME
private static final String ATTR_MANIFEST_VERSION = "Manifest-Version";
private OutputStream os;
private final Manifest manifest;
private final JarInputStream jis;
private JarOutputStream jos;
private Pack200.Packer packer;
private String jarPrefixStr;
private Path jarPrefixFile;
private boolean sealed;
//<editor-fold defaultstate="collapsed" desc="Constructors">
/////////// Constructors ///////////////////////////////////
/**
* Creates a new, empty, JAR
*/
public Jar() {
this.jis = null;
this.manifest = new Manifest();
}
/**
* Reads in the JAR from the given {@code InputStream}.
* Modifications will not be made to the original JAR file, but to a new copy, which is then written with {@link #write(OutputStream) write()}.
*/
public Jar(InputStream jar) throws IOException {
this.jis = jar instanceof JarInputStream ? (JarInputStream) jar : newJarInputStream(jar);
this.manifest = new Manifest(jis.getManifest());
}
/**
* Reads in the JAR from the given {@code Path}.
* Modifications will not be made to the original JAR file, but to a new copy, which is then written with {@link #write(OutputStream) write()}.
*/
public Jar(Path jar) throws IOException {
this.jis = newJarInputStream(Files.newInputStream(jar));
this.manifest = new Manifest(jis.getManifest());
}
/**
* Reads in the JAR from the given {@code File}.
* Modifications will not be made to the original JAR file, but to a new copy, which is then written with {@link #write(OutputStream) write()}.
*/
public Jar(File jar) throws IOException {
this(jar.toPath());
}
/**
* Reads in the JAR from the given path.
* Modifications will not be made to the original JAR file, but to a new copy, which is then written with {@link #write(OutputStream) write()}.
*/
public Jar(String jar) throws IOException {
this(Paths.get(jar));
}
/**
* Creates a copy
*/
public Jar(Jar jar) {
this.jis = jar.jis;
this.manifest = new Manifest(jar.manifest);
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Manifest">
/////////// Manifest ///////////////////////////////////
/**
* Returns the manifest of this JAR. Modifications to the manifest will be reflected in the written JAR, provided they are done
* before any entries are added with {@code addEntry()}.
*/
public Manifest getManifest() {
return manifest;
}
/**
* Sets an attribute in the main section of the manifest.
*
* @param name the attribute's name
* @param value the attribute's value
* @return {@code this}
* @throws IllegalStateException if entries have been added or the JAR has been written prior to calling this methods.
*/
public final Jar setAttribute(String name, String value) {
verifyNotSealed();
if (jos != null)
throw new IllegalStateException("Manifest cannot be modified after entries are added.");
getManifest().getMainAttributes().putValue(name, value);
return this;
}
/**
* Sets an attribute in a non-main section of the manifest.
*
* @param section the section's name
* @param name the attribute's name
* @param value the attribute's value
* @return {@code this}
* @throws IllegalStateException if entries have been added or the JAR has been written prior to calling this methods.
*/
public final Jar setAttribute(String section, String name, String value) {
verifyNotSealed();
if (jos != null)
throw new IllegalStateException("Manifest cannot be modified after entries are added.");
Attributes attr = getManifest().getAttributes(section);
if (attr == null) {
attr = new Attributes();
getManifest().getEntries().put(section, attr);
}
attr.putValue(name, value);
return this;
}
/**
* Sets an attribute in the main section of the manifest to a list.
* The list elements will be joined with a single whitespace character.
*
* @param name the attribute's name
* @param values the attribute's value
* @return {@code this}
* @throws IllegalStateException if entries have been added or the JAR has been written prior to calling this methods.
*/
public Jar setListAttribute(String name, Collection<?> values) {
return setAttribute(name, join(values));
}
/**
* Sets an attribute in a non-main section of the manifest to a list.
* The list elements will be joined with a single whitespace character.
*
* @param section the section's name
* @param name the attribute's name
* @param values the attribute's value
* @return {@code this}
* @throws IllegalStateException if entries have been added or the JAR has been written prior to calling this methods.
*/
public Jar setListAttribute(String section, String name, Collection<?> values) {
return setAttribute(section, name, join(values));
}
/**
* Sets an attribute in the main section of the manifest to a map.
* The map entries will be joined with a single whitespace character, and each key-value pair will be joined with a '='.
*
* @param name the attribute's name
* @param values the attribute's value
* @return {@code this}
* @throws IllegalStateException if entries have been added or the JAR has been written prior to calling this methods.
*/
public Jar setMapAttribute(String name, Map<String, ?> values) {
return setAttribute(name, join(values));
}
/**
* Sets an attribute in a non-main section of the manifest to a map.
* The map entries will be joined with a single whitespace character, and each key-value pair will be joined with a '='.
*
* @param section the section's name
* @param name the attribute's name
* @param values the attribute's value
* @return {@code this}
* @throws IllegalStateException if entries have been added or the JAR has been written prior to calling this methods.
*/
public Jar setMapAttribute(String section, String name, Map<String, ?> values) {
return setAttribute(section, name, join(values));
}
/**
* Returns an attribute's value from this JAR's manifest's main section.
*
* @param name the attribute's name
*/
public String getAttribute(String name) {
return getManifest().getMainAttributes().getValue(name);
}
/**
* Returns an attribute's value from a non-main section of this JAR's manifest.
*
* @param section the manifest's section
* @param name the attribute's name
*/
public String getAttribute(String section, String name) {
Attributes attr = getManifest().getAttributes(section);
return attr != null ? attr.getValue(name) : null;
}
/**
* Returns an attribute's list value from this JAR's manifest's main section.
* The attributes string value will be split on whitespace into the returned list.
* The returned list may be safely modified.
*
* @param name the attribute's name
*/
public List<String> getListAttribute(String name) {
return split(getAttribute(name));
}
/**
* Returns an attribute's list value from a non-main section of this JAR's manifest.
* The attributes string value will be split on whitespace into the returned list.
* The returned list may be safely modified.
*
* @param section the manifest's section
* @param name the attribute's name
*/
public List<String> getListAttribute(String section, String name) {
return split(getAttribute(section, name));
}
/**
* Returns an attribute's map value from this JAR's manifest's main section.
* The attributes string value will be split on whitespace into map entries, and each entry will be split on '=' to get the key-value pair.
* The returned map may be safely modified.
*
* @param name the attribute's name
*/
public Map<String, String> getMapAttribute(String name, String defaultValue) {
return mapSplit(getAttribute(name), defaultValue);
}
/**
* Returns an attribute's map value from a non-main section of this JAR's manifest.
* The attributes string value will be split on whitespace into map entries, and each entry will be split on '=' to get the key-value pair.
* The returned map may be safely modified.
*
* @param section the manifest's section
* @param name the attribute's name
*/
public Map<String, String> getMapAttribute(String section, String name, String defaultValue) {
return mapSplit(getAttribute(section, name), defaultValue);
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Entries">
/////////// Entries ///////////////////////////////////
/**
* Adds an entry to this JAR.
*
* @param path the entry's path within the JAR
* @param is the entry's content
* @return {@code this}
*/
public Jar addEntry(String path, InputStream is) throws IOException {
beginWriting();
addEntry(jos, path, is);
return this;
}
/**
* Adds an entry to this JAR.
*
* @param path the entry's path within the JAR
* @param content the entry's content
* @return {@code this}
*/
public Jar addEntry(String path, byte[] content) throws IOException {
beginWriting();
addEntry(jos, path, content);
return this;
}
/**
* Adds an entry to this JAR.
*
* @param path the entry's path within the JAR
* @param is the entry's content
* @return {@code this}
*/
public Jar addEntry(Path path, InputStream is) throws IOException {
return addEntry(path != null ? path.toString() : "", is);
}
/**
* Adds an entry to this JAR.
*
* @param path the entry's path within the JAR
* @param file the file to add as an entry
* @return {@code this}
*/
public Jar addEntry(Path path, Path file) throws IOException {
return addEntry(path, Files.newInputStream(file));
}
/**
* Adds an entry to this JAR.
*
* @param path the entry's path within the JAR
* @param file the path of the file to add as an entry
* @return {@code this}
*/
public Jar addEntry(Path path, String file) throws IOException {
return addEntry(path, Paths.get(file));
}
/**
* Adds an entry to this JAR.
*
* @param path the entry's path within the JAR
* @param file the file to add as an entry
* @return {@code this}
*/
public Jar addEntry(String path, File file) throws IOException {
try (FileInputStream fos = new FileInputStream(file)) {
return addEntry(path, fos);
}
}
/**
* Adds an entry to this JAR.
*
* @param path the entry's path within the JAR
* @param file the path of the file to add as an entry
* @return {@code this}
*/
public Jar addEntry(String path, String file) throws IOException {
try (FileInputStream fos = new FileInputStream(file)) {
return addEntry(path, fos);
}
}
/**
* Adds a class entry to this JAR.
*
* @param clazz the class to add to the JAR.
* @return {@code this}
*/
public Jar addClass(Class<?> clazz) throws IOException {
final String resource = clazz.getName().replace('.', '/') + ".class";
return addEntry(resource, clazz.getClassLoader().getResourceAsStream(resource));
}
/**
* Adds a directory (with all its subdirectories) or the contents of a zip/JAR to this JAR.
*
* @param path the path within the JAR where the root of the directory will be placed, or {@code null} for the JAR's root
* @param dirOrZip the directory to add as an entry or a zip/JAR file whose contents will be extracted and added as entries
* @param filter a filter to select particular classes
* @return {@code this}
*/
public Jar addEntries(Path path, Path dirOrZip, Filter filter) throws IOException {
if (Files.isDirectory(dirOrZip))
addDir(path, dirOrZip, filter, true);
else {
try (JarInputStream jis1 = newJarInputStream(Files.newInputStream(dirOrZip))) {
addEntries(path, jis1, filter);
}
}
return this;
}
/**
* Adds a directory (with all its subdirectories) or the contents of a zip/JAR to this JAR.
*
* @param path the path within the JAR where the root of the directory will be placed, or {@code null} for the JAR's root
* @param dirOrZip the directory to add as an entry or a zip/JAR file whose contents will be extracted and added as entries
* @return {@code this}
*/
public Jar addEntries(Path path, Path dirOrZip) throws IOException {
return addEntries(path, dirOrZip, null);
}
/**
* Adds a directory (with all its subdirectories) or the contents of a zip/JAR to this JAR.
*
* @param path the path within the JAR where the root of the directory/zip will be placed, or {@code null} for the JAR's root
* @param dirOrZip the directory to add as an entry or a zip/JAR file whose contents will be extracted and added as entries
* @param filter a filter to select particular classes
* @return {@code this}
*/
public Jar addEntries(String path, Path dirOrZip, Filter filter) throws IOException {
return addEntries(path != null ? Paths.get(path) : null, dirOrZip, filter);
}
/**
* Adds a directory (with all its subdirectories) or the contents of a zip/JAR to this JAR.
*
* @param path the path within the JAR where the root of the directory/zip will be placed, or {@code null} for the JAR's root
* @param dirOrZip the directory to add as an entry or a zip/JAR file whose contents will be extracted and added as entries
* @return {@code this}
*/
public Jar addEntries(String path, Path dirOrZip) throws IOException {
return addEntries(path, dirOrZip, null);
}
/**
* Adds the contents of the zip/JAR contained in the given byte array to this JAR.
*
* @param path the path within the JAR where the root of the zip will be placed, or {@code null} for the JAR's root
* @param zip the contents of the zip/JAR file
* @return {@code this}
*/
public Jar addEntries(Path path, ZipInputStream zip) throws IOException {
return addEntries(path, zip, null);
}
/**
* Adds the contents of the zip/JAR contained in the given byte array to this JAR.
*
* @param path the path within the JAR where the root of the zip will be placed, or {@code null} for the JAR's root
* @param zip the contents of the zip/JAR file
* @param filter a filter to select particular classes
* @return {@code this}
*/
public Jar addEntries(Path path, ZipInputStream zip, Filter filter) throws IOException {
beginWriting();
try (ZipInputStream zis = zip) {
for (ZipEntry entry; (entry = zis.getNextEntry()) != null;) {
final String target = path != null ? path.resolve(entry.getName()).toString() : entry.getName();
if (target.equals(MANIFEST_NAME))
continue;
if (filter == null || filter.filter(target))
addEntryNoClose(jos, target, zis);
}
}
return this;
}
/**
* Adds the contents of a Java package to this JAR.
*
* @param clazz A class whose package we wish to add to the JAR.
* @return {@code this}
*/
public Jar addPackageOf(Class<?> clazz) throws IOException {
return addPackageOf(clazz, null);
}
/**
* Adds the contents of a Java package to this JAR.
*
* @param clazz a class whose package we wish to add to the JAR.
* @param filter a filter to select particular classes
* @return {@code this}
*/
public Jar addPackageOf(Class<?> clazz, Filter filter) throws IOException {
try {
final String path = clazz.getPackage().getName().replace('.', '/');
URL dirURL = clazz.getClassLoader().getResource(path);
if (dirURL != null && dirURL.getProtocol().equals("file"))
addDir(Paths.get(path), Paths.get(dirURL.toURI()), filter, false);
else {
if (dirURL == null) // In case of a jar file, we can't actually find a directory.
dirURL = clazz.getClassLoader().getResource(clazz.getName().replace('.', '/') + ".class");
if (dirURL.getProtocol().equals("jar")) {
final URI jarUri = new URI(dirURL.getPath().substring(0, dirURL.getPath().indexOf('!')));
try (JarInputStream jis1 = newJarInputStream(Files.newInputStream(Paths.get(jarUri)))) {
for (JarEntry entry; (entry = jis1.getNextJarEntry()) != null;) {
try {
if (entry.getName().startsWith(path + '/')) {
if (filter == null || filter.filter(entry.getName()))
addEntryNoClose(jos, entry.getName(), jis1);
}
} catch (ZipException e) {
if (!e.getMessage().startsWith("duplicate entry"))
throw e;
}
}
}
} else
throw new AssertionError();
}
return this;
} catch (URISyntaxException e) {
throw new AssertionError(e);
}
}
private void addDir(final Path path, final Path dir1, final Filter filter, final boolean recursive) throws IOException {
final Path dir = dir1.toAbsolutePath();
Files.walkFileTree(dir, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult preVisitDirectory(Path d, BasicFileAttributes attrs) throws IOException {
return (recursive || dir.equals(d)) ? FileVisitResult.CONTINUE : FileVisitResult.SKIP_SUBTREE;
}
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
final Path p = dir.relativize(file.toAbsolutePath());
final Path target = path != null ? path.resolve(p.toString()) : p;
if (!target.toString().equals(MANIFEST_NAME)) {
if (filter == null || filter.filter(target.toString()))
addEntry(target, file);
}
return FileVisitResult.CONTINUE;
}
});
}
private static void addEntry(JarOutputStream jarOut, String path, InputStream is) throws IOException {
jarOut.putNextEntry(new JarEntry(path));
copy(is, jarOut);
jarOut.closeEntry();
}
private static void addEntryNoClose(JarOutputStream jarOut, String path, InputStream is) throws IOException {
jarOut.putNextEntry(new JarEntry(path));
copy0(is, jarOut);
jarOut.closeEntry();
}
private static void addEntry(JarOutputStream jarOut, String path, byte[] data) throws IOException {
jarOut.putNextEntry(new JarEntry(path));
jarOut.write(data);
jarOut.flush();
jarOut.closeEntry();
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Jar File Properties">
/////////// Jar File Properties ///////////////////////////////////
/**
* Sets a {@link Pack200} packer to use when writing the JAR.
*
* @param packer
* @return {@code this}
*/
public Jar setPacker(Pack200.Packer packer) {
this.packer = packer;
return this;
}
/**
* If set to true true, a header will be added to the JAR file when written, that will make the JAR an executable file in POSIX environments.
*
* @param value
* @return {@code this}
*/
public Jar setReallyExecutable(boolean value) {
setJarPrefix(value ? "#!/bin/sh\n\nexec java -jar $0 \"$@\"\n" : null);
return this;
}
/**
* Sets a string that will be prepended to the JAR file's data.
*
* @param value the prefix, or {@code null} for none.
* @return {@code this}
*/
public Jar setJarPrefix(String value) {
verifyNotSealed();
if (jos != null)
throw new IllegalStateException("Really executable cannot be set after entries are added.");
if (value != null && jarPrefixFile != null)
throw new IllegalStateException("A prefix has already been set (" + jarPrefixFile + ")");
this.jarPrefixStr = value;
return this;
}
/**
* Sets a file whose contents will be prepended to the JAR file's data.
*
* @param file the prefix file, or {@code null} for none.
* @return {@code this}
*/
public Jar setJarPrefix(Path file) {
verifyNotSealed();
if (jos != null)
throw new IllegalStateException("Really executable cannot be set after entries are added.");
if (file != null && jarPrefixStr != null)
throw new IllegalStateException("A prefix has already been set (" + jarPrefixStr + ")");
this.jarPrefixFile = file;
return this;
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Writing">
/////////// Writing ///////////////////////////////////
/**
* Sets an {@link OutputStream} to which the JAR will be written.
* If used, this method must be called before any entries have been added or the JAR written. Calling this method prevents this
* object from using an internal buffer to store the JAR, and therefore, none of the other {@code write} methods can be called.
*
* @param os the target OutputStream of this JAR.
* @return {@code this}
*/
public Jar setOutputStream(OutputStream os) {
if (os == null)
throw new NullPointerException("The OutputStream is null");
if (jos != null)
throw new IllegalStateException("Entries have already been added, the JAR has been written or setOutputStream has already been called.");
this.os = os;
return this;
}
/**
* Same as {@link #setOutputStream(OutputStream) setOutputStream(Files.newOutputStream(out))}.
* If used, this method must be called before any entries have been added or the JAR written. Calling this method prevents this
* object from using an internal buffer to store the JAR, and therefore, none of the other {@code write} methods can be called.
*
* @param out the target file to which this JAR will be written.
* @return {@code this}
*/
public Jar setOutput(Path out) throws IOException {
return setOutputStream(Files.newOutputStream(out));
}
/**
* Same as {@link #setOutputStream(OutputStream) setOutputStream(new FileOutputStream(out))}.
* If used, this method must be called before any entries have been added or the JAR written. Calling this method prevents this
* object from using an internal buffer to store the JAR, and therefore, none of the other {@code write} methods can be called.
*
* @param out the target file to which this JAR will be written.
* @return {@code this}
*/
public Jar setOutput(File out) throws IOException {
return setOutputStream(new FileOutputStream(out));
}
private void beginWriting() throws IOException {
verifyNotSealed();
if (jos != null)
return;
if (os == null)
this.os = new ByteArrayOutputStream();
writePrefix(os);
if (getAttribute(ATTR_MANIFEST_VERSION) == null)
setAttribute(ATTR_MANIFEST_VERSION, "1.0");
jos = new JarOutputStream(os, manifest);
if (jis != null)
addEntries(null, jis);
}
private void writePrefix(OutputStream os) throws IOException {
if (jarPrefixStr != null) {
final Writer out = new OutputStreamWriter(os, UTF_8);
out.write(jarPrefixStr);
out.flush();
} else if (jarPrefixFile != null)
Files.copy(jarPrefixFile, os);
if (jarPrefixStr != null || jarPrefixFile != null) {
os.write('\n');
os.flush();
}
}
public Jar close() throws IOException {
if (sealed)
return this;
beginWriting();
// writeManifest(); - some JDK Jar classes (like JarInputStream) assume that the manifest must be the first entry
jos.close();
this.sealed = true;
return this;
}
/**
* Writes this JAR to an output stream, and closes the stream.
*/
public <T extends OutputStream> T write(T os) throws IOException {
close();
if (!(this.os instanceof ByteArrayOutputStream))
throw new IllegalStateException("Cannot write to another target if setOutputStream has been called");
final byte[] content = ((ByteArrayOutputStream) this.os).toByteArray();
if (packer != null)
packer.pack(new JarInputStream(new ByteArrayInputStream(content)), os);
else
os.write(content);
os.close();
return os;
}
private void verifyNotSealed() {
if (sealed)
throw new IllegalStateException("This JAR has been sealed (when it was written)");
}
/**
* Writes this JAR to a file.
*/
public File write(File file) throws IOException {
try (FileOutputStream fos = new FileOutputStream(file)) {
write(fos);
return file;
}
}
/**
* Writes this JAR to a file.
*/
public Path write(Path path) throws IOException {
write(Files.newOutputStream(path));
return path;
}
/**
* Writes this JAR to a file.
*/
public void write(String file) throws IOException {
write(Paths.get(file));
}
/**
* Returns this JAR file as an array of bytes.
*/
public byte[] toByteArray() {
try {
return write(new ByteArrayOutputStream()).toByteArray();
} catch (IOException e) {
throw new AssertionError();
}
}
//</editor-fold>
/**
* Turns a {@code String} into an {@code InputStream} containing the string's encoded characters.
*
* @param str the string
* @param charset the {@link Charset} to use when encoding the string.
* @return an {@link InputStream} containing the string's encoded characters.
*/
public static InputStream toInputStream(String str, Charset charset) {
return new ByteArrayInputStream(str.getBytes(charset));
}
//<editor-fold defaultstate="collapsed" desc="Filter">
/////////// Filter ///////////////////////////////////
public static interface Filter {
boolean filter(String entryName);
}
public static Filter matches(String regex) {
final Pattern p = Pattern.compile(regex);
return new Filter() {
@Override
public boolean filter(String entryName) {
return p.matcher(entryName).matches();
}
};
}
public static Filter notMatches(String regex) {
final Filter f = matches(regex);
return new Filter() {
@Override
public boolean filter(String entryName) {
return !f.filter(entryName);
}
};
}
//</editor-fold>
//<editor-fold defaultstate="collapsed" desc="Utils">
/////////// Utils ///////////////////////////////////
private static JarInputStream newJarInputStream(InputStream in) throws IOException {
return new co.paralleluniverse.common.JarInputStream(in);
}
private static ZipInputStream newZipInputStream(InputStream in) throws IOException {
return new co.paralleluniverse.common.ZipInputStream(in);
}
private static void copy(InputStream is, OutputStream os) throws IOException {
try {
copy0(is, os);
} finally {
is.close();
}
}
private static void copy0(InputStream is, OutputStream os) throws IOException {
final byte[] buffer = new byte[1024];
for (int bytesRead; (bytesRead = is.read(buffer)) != -1;)
os.write(buffer, 0, bytesRead);
os.flush();
}
private static String join(Collection<?> list, String separator) {
if (list == null)
return null;
StringBuilder sb = new StringBuilder();
for (Object element : list)
sb.append(element.toString()).append(separator);
if (!list.isEmpty())
sb.delete(sb.length() - separator.length(), sb.length());
return sb.toString();
}
private static String join(Map<String, ?> map, char kvSeparator, String separator) {
if (map == null)
return null;
StringBuilder sb = new StringBuilder();
for (Map.Entry<String, ?> entry : map.entrySet())
sb.append(entry.getKey()).append(kvSeparator).append(entry.getValue().toString()).append(separator);
if (!map.isEmpty())
sb.delete(sb.length() - separator.length(), sb.length());
return sb.toString();
}
private static String join(Collection<?> list) {
return join(list, " ");
}
private static String join(Map<String, ?> map) {
return join(map, '=', " ");
}
private static List<String> split(String str, String separator) {
if (str == null)
return null;
String[] es = str.split(separator);
final List<String> list = new ArrayList<>(es.length);
for (String e : es) {
e = e.trim();
if (!e.isEmpty())
list.add(e);
}
return list;
}
private static List<String> split(String list) {
return split(list, " ");
}
private static Map<String, String> split(String map, char kvSeparator, String separator, String defaultValue) {
if (map == null)
return null;
Map<String, String> m = new HashMap<>();
for (String entry : split(map, separator)) {
final String key = getBefore(entry, kvSeparator);
String value = getAfter(entry, kvSeparator);
if (value == null) {
if (defaultValue != null)
value = defaultValue;
else
throw new IllegalArgumentException("Element " + entry + " in \"" + map + "\" is not a key-value entry separated with " + kvSeparator + " and no default value provided");
}
m.put(key, value);
}
return m;
}
private static Map<String, String> mapSplit(String map, String defaultValue) {
return split(map, '=', " ", defaultValue);
}
private static String getBefore(String s, char separator) {
final int i = s.indexOf(separator);
if (i < 0)
return s;
return s.substring(0, i);
}
private static String getAfter(String s, char separator) {
final int i = s.indexOf(separator);
if (i < 0)
return null;
return s.substring(i + 1);
}
private static Path nullPath() {
return null;
}
//</editor-fold>
}