package be.nikiroo.utils;
-import java.io.ByteArrayInputStream;
import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.text.Normalizer.Form;
import java.text.ParseException;
import java.text.SimpleDateFormat;
+import java.util.AbstractMap;
+import java.util.ArrayList;
import java.util.Date;
import java.util.List;
-import java.util.Scanner;
+import java.util.Map.Entry;
import java.util.regex.Pattern;
import org.unbescape.html.HtmlEscape;
}
}
+ /**
+ * Justify a text into width-sized (at the maximum) lines.
+ *
+ * @param text
+ * the {@link String} to justify
+ * @param width
+ * the maximum size of the resulting lines
+ *
+ * @return a list of justified text lines
+ */
+ static public List<String> justifyText(List<String> text, int width) {
+ return justifyText(text, width, null);
+ }
+
+ /**
+ * Justify a text into width-sized (at the maximum) lines.
+ *
+ * @param text
+ * the {@link String} to justify
+ * @param width
+ * the maximum size of the resulting lines
+ * @param align
+ * align the lines in this position (default is
+ * Alignment.Beginning)
+ *
+ * @return a list of justified text lines
+ */
+ static public List<String> justifyText(List<String> text, int width,
+ Alignment align) {
+ List<String> result = new ArrayList<String>();
+
+ // Content <-> Bullet spacing (null = no spacing)
+ List<Entry<String, String>> lines = new ArrayList<Entry<String, String>>();
+ StringBuilder previous = null;
+ StringBuilder tmp = new StringBuilder();
+ String previousItemBulletSpacing = null;
+ String itemBulletSpacing = null;
+ for (String inputLine : text) {
+ boolean previousLineComplete = true;
+
+ String current = inputLine.replace("\t", " ");
+ itemBulletSpacing = getItemSpacing(current);
+ boolean bullet = isItemLine(current);
+ if ((previousItemBulletSpacing == null || itemBulletSpacing
+ .length() <= previousItemBulletSpacing.length()) && !bullet) {
+ itemBulletSpacing = null;
+ }
+
+ if (itemBulletSpacing != null) {
+ current = current.trim();
+ if (!current.isEmpty() && bullet) {
+ current = current.substring(1);
+ }
+ current = current.trim();
+ previousLineComplete = bullet;
+ } else {
+ tmp.setLength(0);
+ for (String word : current.split(" ")) {
+ if (word.isEmpty()) {
+ continue;
+ }
+
+ if (tmp.length() > 0) {
+ tmp.append(' ');
+ }
+ tmp.append(word.trim());
+ }
+ current = tmp.toString();
+
+ previousLineComplete = current.isEmpty()
+ || previousItemBulletSpacing != null
+ || (previous != null && isFullLine(previous))
+ || isHrLine(current) || isHrLine(previous);
+ }
+
+ if (previous == null) {
+ previous = new StringBuilder();
+ } else {
+ if (previousLineComplete) {
+ lines.add(new AbstractMap.SimpleEntry<String, String>(
+ previous.toString(), previousItemBulletSpacing));
+ previous.setLength(0);
+ previousItemBulletSpacing = itemBulletSpacing;
+ } else {
+ previous.append(' ');
+ }
+ }
+
+ previous.append(current);
+
+ }
+
+ if (previous != null) {
+ lines.add(new AbstractMap.SimpleEntry<String, String>(previous
+ .toString(), previousItemBulletSpacing));
+ }
+
+ for (Entry<String, String> line : lines) {
+ String content = line.getKey();
+ String spacing = line.getValue();
+
+ String bullet = "- ";
+ if (spacing == null) {
+ bullet = "";
+ spacing = "";
+ }
+
+ if (spacing.length() > width + 3) {
+ spacing = "";
+ }
+
+ for (String subline : StringUtils.justifyText(content, width
+ - (spacing.length() + bullet.length()), align)) {
+ result.add(spacing + bullet + subline);
+ if (!bullet.isEmpty()) {
+ bullet = " ";
+ }
+ }
+ }
+
+ return result;
+ }
+
/**
* Sanitise the given input to make it more Terminal-friendly by removing
* combining characters.
/**
* Zip the data and then encode it into Base64.
*
+ * @deprecated use {@link StringUtils#base64(byte[], boolean)} with the
+ * correct parameter instead
+ *
* @param data
* the data
*
* @return the Base64 zipped version
*/
+ @Deprecated
public static String zip64(String data) {
try {
- return Base64.encodeBytes(data.getBytes(), Base64.GZIP);
+ return Base64.encodeBytes(data.getBytes("UTF-8"), Base64.GZIP);
} catch (IOException e) {
e.printStackTrace();
return null;
/**
* Unconvert from Base64 then unzip the content.
*
+ * @deprecated use {@link StringUtils#unbase64s(String, boolean)} with the
+ * correct parameter instead
+ *
* @param data
* the data in Base64 format
*
* @throws IOException
* in case of I/O error
*/
+ @Deprecated
public static String unzip64(String data) throws IOException {
- ByteArrayInputStream in = new ByteArrayInputStream(Base64.decode(data,
- Base64.GZIP));
+ return new String(Base64.decode(data, Base64.GZIP), "UTF-8");
+ }
- Scanner scan = new Scanner(in);
- scan.useDelimiter("\\A");
- try {
- return scan.next();
- } finally {
- scan.close();
+ /**
+ * Convert the given data to Base64 format.
+ *
+ * @param data
+ * the data to convert
+ * @param zip
+ * TRUE to also compress the data in GZIP format; remember that
+ * compressed and not-compressed content are different; you need
+ * to know which is which when decoding
+ *
+ * @return the Base64 {@link String} representation of the data
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static String base64(String data, boolean zip) throws IOException {
+ return base64(data.getBytes("UTF-8"), zip);
+ }
+
+ /**
+ * Convert the given data to Base64 format.
+ *
+ * @param data
+ * the data to convert
+ * @param zip
+ * TRUE to also compress the data in GZIP format; remember that
+ * compressed and not-compressed content are different; you need
+ * to know which is which when decoding
+ *
+ * @return the Base64 {@link String} representation of the data
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static String base64(byte[] data, boolean zip) throws IOException {
+ return Base64.encodeBytes(data, zip ? Base64.GZIP : Base64.NO_OPTIONS);
+ }
+
+ /**
+ * Convert the given data to Base64 format.
+ *
+ * @param data
+ * the data to convert
+ * @param zip
+ * TRUE to also uncompress the data from a GZIP format; take care
+ * about this flag, as it could easily cause errors in the
+ * returned content or an {@link IOException}
+ * @param breakLines
+ * TRUE to break lines on every 76th character
+ *
+ * @return the Base64 {@link String} representation of the data
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static OutputStream base64(OutputStream data, boolean zip,
+ boolean breakLines) throws IOException {
+ OutputStream out = new Base64.OutputStream(data,
+ breakLines ? Base64.DO_BREAK_LINES & Base64.ENCODE
+ : Base64.ENCODE);
+
+ if (zip) {
+ out = new java.util.zip.GZIPOutputStream(out);
+ }
+
+ return out;
+ }
+
+ /**
+ * Convert the given data to Base64 format.
+ *
+ * @param data
+ * the data to convert
+ * @param zip
+ * TRUE to also uncompress the data from a GZIP format; take care
+ * about this flag, as it could easily cause errors in the
+ * returned content or an {@link IOException}
+ * @param breakLines
+ * TRUE to break lines on every 76th character
+ *
+ * @return the Base64 {@link String} representation of the data
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static InputStream base64(InputStream data, boolean zip,
+ boolean breakLines) throws IOException {
+ if (zip) {
+ data = new java.util.zip.GZIPInputStream(data);
}
+
+ return new Base64.InputStream(data, breakLines ? Base64.DO_BREAK_LINES
+ & Base64.ENCODE : Base64.ENCODE);
+ }
+
+ /**
+ * Unconvert the given data from Base64 format back to a raw array of bytes.
+ * <p>
+ * Will automatically detect zipped data and also uncompress it before
+ * returning, unless ZIP is false.
+ *
+ * @param data
+ * the data to unconvert
+ * @param zip
+ * TRUE to also uncompress the data from a GZIP format
+ * automatically; if set to FALSE, zipped data can be returned
+ *
+ * @return the raw data represented by the given Base64 {@link String},
+ * optionally compressed with GZIP
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static byte[] unbase64(String data, boolean zip) throws IOException {
+ return Base64
+ .decode(data, zip ? Base64.NO_OPTIONS : Base64.DONT_GUNZIP);
+ }
+
+ /**
+ * Unconvert the given data from Base64 format back to a raw array of bytes.
+ *
+ * @param data
+ * the data to unconvert
+ * @param zip
+ * TRUE to also uncompress the data from a GZIP format; take care
+ * about this flag, as it could easily cause errors in the
+ * returned content or an {@link IOException}
+ *
+ * @return the raw data represented by the given Base64 {@link String}
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static OutputStream unbase64(OutputStream data, boolean zip)
+ throws IOException {
+ OutputStream out = new Base64.OutputStream(data, Base64.DECODE);
+
+ if (zip) {
+ out = new java.util.zip.GZIPOutputStream(out);
+ }
+
+ return out;
+ }
+
+ /**
+ * Unconvert the given data from Base64 format back to a raw array of bytes.
+ *
+ * @param data
+ * the data to unconvert
+ * @param zip
+ * TRUE to also uncompress the data from a GZIP format; take care
+ * about this flag, as it could easily cause errors in the
+ * returned content or an {@link IOException}
+ *
+ * @return the raw data represented by the given Base64 {@link String}
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static InputStream unbase64(InputStream data, boolean zip)
+ throws IOException {
+ if (zip) {
+ data = new java.util.zip.GZIPInputStream(data);
+ }
+
+ return new Base64.InputStream(data, Base64.DECODE);
+ }
+
+ /**
+ * Unconvert the given data from Base64 format back to a raw array of bytes.
+ * <p>
+ * Will automatically detect zipped data and also uncompress it before
+ * returning, unless ZIP is false.
+ *
+ * @param data
+ * the data to unconvert
+ * @param offset
+ * the offset at which to start taking the data (do not take the
+ * data before it into account)
+ * @param count
+ * the number of bytes to take into account (do not process after
+ * this number of bytes has been processed)
+ * @param zip
+ * TRUE to also uncompress the data from a GZIP format
+ * automatically; if set to FALSE, zipped data can be returned
+ *
+ * @return the raw data represented by the given Base64 {@link String}
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static byte[] unbase64(byte[] data, int offset, int count,
+ boolean zip) throws IOException {
+ return Base64.niki_decode(data, offset, count, zip ? Base64.NO_OPTIONS
+ : Base64.DONT_GUNZIP);
+ }
+
+ /**
+ * Unonvert the given data from Base64 format back to a {@link String}.
+ * <p>
+ * Will automatically detect zipped data and also uncompress it before
+ * returning, unless ZIP is false.
+ *
+ * @param data
+ * the data to unconvert
+ * @param zip
+ * TRUE to also uncompress the data from a GZIP format
+ * automatically; if set to FALSE, zipped data can be returned
+ *
+ * @return the {@link String} represented by the given Base64 {@link String}
+ * , optionally compressed with GZIP
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static String unbase64s(String data, boolean zip) throws IOException {
+ return new String(unbase64(data, zip), "UTF-8");
+ }
+
+ /**
+ * Unconvert the given data from Base64 format back into a {@link String}.
+ *
+ * @param data
+ * the data to unconvert
+ * @param offset
+ * the offset at which to start taking the data (do not take the
+ * data before it into account)
+ * @param count
+ * the number of bytes to take into account (do not process after
+ * this number of bytes has been processed)
+ * @param zip
+ * TRUE to also uncompress the data from a GZIP format; take care
+ * about this flag, as it could easily cause errors in the
+ * returned content or an {@link IOException}
+ *
+ * @return the {@link String} represented by the given Base64 {@link String}
+ * , optionally compressed with GZIP
+ *
+ * @throws IOException
+ * in case of I/O errors
+ */
+ public static String unbase64s(byte[] data, int offset, int count,
+ boolean zip) throws IOException {
+ return new String(unbase64(data, offset, count, zip), "UTF-8");
+ }
+
+ /**
+ * Return a display {@link String} for the given value, which can be
+ * suffixed with "k" or "M" depending upon the number, if it is big enough.
+ * <p>
+ * Example:
+ * <ul>
+ * <li><tt>8765</tt> becomes "8k"</li>
+ * <li><tt>998765</tt> becomes "998k"</li>
+ * <li><tt>12987364</tt> becomes "12M"</li>
+ * </ul>
+ *
+ * @param value
+ * the value to convert
+ *
+ * @return the display value
+ */
+ public static String formatNumber(long value) {
+ return formatNumber(value, true);
+ }
+
+ /**
+ * Return a display {@link String} for the given value, which can be
+ * suffixed with "k" or "M" depending upon the number, if it is big enough.
+ * <p>
+ * Example:
+ * <ul>
+ * <li><tt>8765</tt> becomes "8k"</li>
+ * <li><tt>998765</tt> becomes "998k"</li>
+ * <li><tt>12987364</tt> becomes "12M"</li>
+ * </ul>
+ *
+ * @param value
+ * the value to convert
+ * @param strict
+ * TRUE if you want any value equals or greater than 1000 to use
+ * the "k" suffix; FALSE if you want to go a bit higher and only
+ * use "k" for values equal or greater than 4000
+ *
+ * @return the display value
+ */
+ public static String formatNumber(long value, boolean strict) {
+ if (value >= 1000000l) {
+ return Long.toString(value / 1000000l) + "M";
+ }
+
+ if ((strict && value >= 1000l) || (!strict && value >= 4000l)) {
+ return Long.toString(value / 1000l) + "k";
+ }
+
+ return Long.toString(value);
}
/**
return null;
}
}
+
+ //
+ // justify List<String> related:
+ //
+
+ /**
+ * Check if this line ends as a complete line (ends with a "." or similar).
+ * <p>
+ * Note that we consider an empty line as full, and a line ending with
+ * spaces as not complete.
+ *
+ * @param line
+ * the line to check
+ *
+ * @return TRUE if it does
+ */
+ static private boolean isFullLine(StringBuilder line) {
+ if (line.length() == 0) {
+ return true;
+ }
+
+ char lastCar = line.charAt(line.length() - 1);
+ switch (lastCar) {
+ case '.': // points
+ case '?':
+ case '!':
+
+ case '\'': // quotes
+ case '‘':
+ case '’':
+
+ case '"': // double quotes
+ case '”':
+ case '“':
+ case '»':
+ case '«':
+ return true;
+ default:
+ return false;
+ }
+ }
+
+ /**
+ * Check if this line represent an item in a list or description (i.e.,
+ * check that the first non-space char is "-").
+ *
+ * @param line
+ * the line to check
+ *
+ * @return TRUE if it is
+ */
+ static private boolean isItemLine(String line) {
+ String spacing = getItemSpacing(line);
+ return spacing != null && !spacing.isEmpty()
+ && line.charAt(spacing.length()) == '-';
+ }
+
+ /**
+ * Return all the spaces that start this line (or Empty if none).
+ *
+ * @param line
+ * the line to get the starting spaces from
+ *
+ * @return the left spacing
+ */
+ static private String getItemSpacing(String line) {
+ int i;
+ for (i = 0; i < line.length(); i++) {
+ if (line.charAt(i) != ' ') {
+ return line.substring(0, i);
+ }
+ }
+
+ return "";
+ }
+
+ /**
+ * This line is an horizontal spacer line.
+ *
+ * @param line
+ * the line to test
+ *
+ * @return TRUE if it is
+ */
+ static private boolean isHrLine(CharSequence line) {
+ int count = 0;
+ if (line != null) {
+ for (int i = 0; i < line.length(); i++) {
+ char car = line.charAt(i);
+ if (car == ' ' || car == '\t' || car == '*' || car == '-'
+ || car == '_' || car == '~' || car == '=' || car == '/'
+ || car == '\\') {
+ count++;
+ } else {
+ return false;
+ }
+ }
+ }
+
+ return count > 2;
+ }
}