001/*
002 * #%L
003 * Netarchivesuite - common
004 * %%
005 * Copyright (C) 2005 - 2014 The Royal Danish Library, the Danish State and University Library,
006 *             the National Library of France and the Austrian National Library.
007 * %%
008 * This program is free software: you can redistribute it and/or modify
009 * it under the terms of the GNU Lesser General Public License as
010 * published by the Free Software Foundation, either version 2.1 of the
011 * License, or (at your option) any later version.
012 * 
013 * This program is distributed in the hope that it will be useful,
014 * but WITHOUT ANY WARRANTY; without even the implied warranty of
015 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
016 * GNU General Lesser Public License for more details.
017 * 
018 * You should have received a copy of the GNU General Lesser Public
019 * License along with this program.  If not, see
020 * <http://www.gnu.org/licenses/lgpl-2.1.html>.
021 * #L%
022 */
023package dk.netarkivet.common.utils;
024
025import java.text.DecimalFormat;
026import java.text.SimpleDateFormat;
027import java.util.ArrayList;
028import java.util.Collection;
029import java.util.Date;
030import java.util.List;
031
032import dk.netarkivet.common.exceptions.ArgumentNotValid;
033
034/**
035 * Utilities for working with strings.
036 */
037public final class StringUtils {
038
039    /**
040     * Utility class, do not initialise.
041     */
042    private StringUtils() {
043    }
044
045    /**
046     * Replace all occurrences of oldString with newString in a string.
047     *
048     * @param sentence the string, where all occurrences of oldString are to be replaced with newString.
049     * @param oldString the oldString.
050     * @param newString the newString.
051     * @return the resulting string, where all occurrences of oldString are replaced with newString.
052     */
053    public static String replace(String sentence, String oldString, String newString) {
054        StringBuilder newStr = new StringBuilder();
055        int found = 0;
056        int lastPointer = 0;
057        do {
058            found = sentence.indexOf(oldString, lastPointer);
059
060            if (found < 0) {
061                newStr.append(sentence.substring(lastPointer, sentence.length()));
062            } else {
063                if (found > lastPointer) {
064                    newStr.append(sentence.substring(lastPointer, found));
065                    newStr.append(newString);
066                    lastPointer = found + oldString.length();
067                }
068            }
069        } while (found > -1);
070        return newStr.toString();
071    }
072
073    /**
074     * Concatenate all objects in a collection with the given separator between each. If the Collection is a List, this
075     * method will generate the conjoined string in list order. If the objects are not Strings, the toString method will
076     * be used to convert them to strings.
077     *
078     * @param sep A string to separate the list items.
079     * @param objects A collection of object to concatenate as a string.
080     * @param <T> The type of objects to conjoin.
081     * @return The concatenated string, or null if the list was null.
082     */
083    public static <T> String conjoin(String sep, Collection<T> objects) {
084        if (objects == null) {
085            return null;
086        }
087        StringBuilder res = new StringBuilder();
088        for (T o : objects) {
089            if (res.length() != 0) {
090                res.append(sep);
091            }
092            res.append(o);
093        }
094        return res.toString();
095    }
096
097    /**
098     * Concatenate the string representation of a maximum number of objects in a collection with a given separator
099     * between them. If the Collection is a List, this method will generate the conjoined string in list order. If the
100     * objects are not Strings, the toString method will be used to convert them to strings.
101     *
102     * @param <T> The type of collection.
103     * @param separator The string to separate the entries in the collection with. This is allowed to be the empty
104     * string.
105     * @param objects The collection to have the string representation of its entries concatenated.
106     * @param max The maximum number of objects in the collection to concatenate. If this number is 0 or below only the
107     * first entry in the collection is returned.
108     * @return The concatenation of the string representation of a limited amount of entries in the collection.
109     * @throws ArgumentNotValid If the separator or the objects are null.
110     */
111    public static <T> String conjoin(String separator, Collection<T> objects, int max) throws ArgumentNotValid {
112        ArgumentNotValid.checkNotNull(separator, "String separator");
113        ArgumentNotValid.checkNotNull(objects, "Collection<T> objects");
114
115        StringBuilder res = new StringBuilder();
116        int index = 0;
117        // go through all the objects.
118        for (T o : objects) {
119            if (res.length() != 0) {
120                res.append(separator);
121            }
122            res.append(o);
123
124            // check if max is reached.
125            ++index;
126            if (index > max) {
127                break;
128            }
129        }
130
131        return res.toString();
132    }
133
134    /**
135     * Concatenate all strings in a collection with the given separator between each.
136     *
137     * @param sep A string to separate the list items.
138     * @param strings An array of strings to concatenate.
139     * @return The concatenated string, or null if the list was null.
140     */
141    public static String conjoin(String sep, String... strings) {
142        if (strings == null) {
143            return null;
144        }
145        StringBuilder res = new StringBuilder();
146        for (String s : strings) {
147            if (res.length() != 0) {
148                res.append(sep);
149            }
150            res.append(s);
151        }
152        return res.toString();
153    }
154
155    /**
156     * Concatenate all strings in a collection, with the fixed strings appended and prepended to each.
157     *
158     * @param strings A list of strings to join up.
159     * @param pre A string that will be put in front of each string in the list.
160     * @param post A string that will be put after each string in the list.
161     * @return The joined string, or null if strings is null.
162     */
163    public static String surjoin(List<String> strings, String pre, String post) {
164        if (strings == null) {
165            return null;
166        }
167        StringBuilder res = new StringBuilder();
168        for (String s : strings) {
169            res.append(pre);
170            res.append(s);
171            res.append(post);
172        }
173        return res.toString();
174    }
175
176    /**
177     * Repeat the string n times.
178     *
179     * @param s A string to repeat.
180     * @param n How many times to repeat it.
181     * @return A repeated string.
182     * @throws ArgumentNotValid if a negative amount is specified.
183     */
184    public static String repeat(String s, int n) {
185        ArgumentNotValid.checkNotNegative(n, "int n");
186        StringBuilder sb = new StringBuilder();
187        for (int i = 0; i < n; i++) {
188            sb.append(s);
189        }
190        return sb.toString();
191    }
192
193    /**
194     * Change all Strings to Integers.
195     *
196     * @param stringArray the given array of Strings to convert.
197     * @return a List of Integers.
198     */
199    public static List<Integer> parseIntList(String[] stringArray) {
200        List<Integer> resultList = new ArrayList<Integer>();
201        for (String element : stringArray) {
202            try {
203                resultList.add(Integer.parseInt(element));
204            } catch (NumberFormatException e) {
205                throw new ArgumentNotValid("Unable to parse '" + element + "' as int");
206            }
207        }
208        return resultList;
209    }
210
211    /**
212     * Generate a ellipsis of orgString. If orgString is longer than maxLength, then we return a String containing the
213     * first maxLength characters and then append " ..".
214     *
215     * @param orgString the original string.
216     * @param maxLength the maximum length of the string before ellipsing it.
217     * @return an ellipsis of orgString.
218     */
219    public static String makeEllipsis(String orgString, int maxLength) {
220        ArgumentNotValid.checkNotNull(orgString, "String orgString");
221        String resultString = orgString;
222        if (orgString.length() > maxLength) {
223            resultString = orgString.substring(0, maxLength - 1) + " ..";
224        }
225        return resultString;
226    }
227
228    /** A day in seconds. */
229    private static final long DAY = 60 * 60 * 24;
230    /** An hour in seconds. */
231    private static final long HOUR = 60 * 60;
232    /** A minute in seconds. */
233    private static final long MINUTE = 60;
234
235    /** Formats a decimal number. */
236    private static final DecimalFormat DECIMAL = new DecimalFormat("###.##");
237
238    /** Default date format : yyyy/MM/dd HH:mm:ss */
239    private static final SimpleDateFormat DEFAULT_DATE = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
240
241    /**
242     * Formats a duration in seconds as a string of the form "3d 04:12:56".
243     *
244     * @param seconds A duration in seconds
245     * @return a formatted string of the form "3d 04:12:56"
246     */
247    public static String formatDuration(long seconds) {
248        if (seconds > 0L) {
249            long lRest;
250
251            String strDays = formatDurationLpad(String.valueOf(seconds / DAY)) + "d ";
252            lRest = seconds % DAY;
253
254            String strHours = formatDurationLpad(String.valueOf(lRest / HOUR)) + ":";
255            lRest %= HOUR;
256
257            String strMinutes = formatDurationLpad(String.valueOf(lRest / MINUTE)) + ":";
258            lRest %= MINUTE;
259
260            String strSeconds = formatDurationLpad(String.valueOf(lRest));
261
262            return strDays + strHours + strMinutes + strSeconds;
263
264        } else if (seconds == 0L) {
265            return "0d 00:00:00";
266        } else {
267            return "-1";
268        }
269    }
270
271    /**
272     * Leftpad the string with "0", if the string is only one character long.
273     *
274     * @param s The given string
275     * @return Return a string leftpadded with a "0" if the string is only one character long, Otherwise just return the
276     * string.
277     */
278    private static String formatDurationLpad(final String s) {
279        return (s.length() == 1 ? "0" + s : s);
280    }
281
282    /**
283     * Formats a numeric percentage, as a decimal number with at most 2 digits.
284     *
285     * @param percentage the numeric percentage to format.
286     * @return a formatted percentage string.
287     */
288    public static String formatPercentage(double percentage) {
289        return formatNumber(percentage) + "%";
290    }
291
292    /**
293     * Formats a numeric percentage, as a decimal number with at most 2 digits.
294     *
295     * @param percentage the numeric percentage to format.
296     * @return a formatted percentage string.
297     */
298    public static String formatPercentage(long percentage) {
299        return formatNumber(percentage) + "%";
300    }
301
302    /**
303     * Formats a number, as a decimal number with at most 2 digits.
304     *
305     * @param number the number to format.
306     * @return a formatted number string.
307     */
308    public static String formatNumber(double number) {
309        return DECIMAL.format(number);
310    }
311
312    /**
313     * Formats a number, as a decimal number with at most 2 digits.
314     *
315     * @param number the number to format.
316     * @return a formatted number string.
317     */
318    public static String formatNumber(long number) {
319        return DECIMAL.format(number);
320    }
321
322    /**
323     * Formats the given date (as elapsed milliseconds) using the default format 'yyyy/MM/dd HH:mm:ss'.
324     *
325     * @param millis the date
326     * @return a formatted date string
327     */
328    public synchronized static String formatDate(long millis) {
329        return DEFAULT_DATE.format(new Date(millis));
330    }
331
332    /**
333     * Formats the given date (as elapsed milliseconds) using the provided format pattern.
334     *
335     * @param millis the date
336     * @param format the format pattern {@link SimpleDateFormat}
337     * @return a formatted date string
338     */
339    public static String formatDate(long millis, String format) {
340        return new SimpleDateFormat(format).format(new Date(millis));
341    }
342
343    /**
344     * Given an input String, this method splits the String with newlines into a multiline String with line-lengths
345     * approximately lineLength. The split is made at the first blank space found at more than lineLength characters
346     * after the previous split.
347     *
348     * @param input the input String.
349     * @param lineLength the desired line length.
350     * @return the split String.
351     * @throws ArgumentNotValid if the input is null or the lineLength is not positive
352     */
353    public static String splitStringOnWhitespace(String input, int lineLength) {
354        ArgumentNotValid.checkNotNull(input, "input");
355        ArgumentNotValid.checkPositive(lineLength, "lineLength");
356        input = input.trim();
357        String[] inputLines = input.split("\n");
358        StringBuffer output = new StringBuffer();
359        for (int i = 0; i < inputLines.length; i++) {
360            int foundIndex = 0;
361            String inputLine = inputLines[i];
362            while (foundIndex != -1) {
363                foundIndex = inputLine.indexOf(" ", foundIndex + lineLength);
364                // We split after the found blank space so check that this is
365                // meaningful.
366                if (foundIndex != -1 && inputLine.length() > foundIndex + 1) {
367                    inputLine = inputLine.substring(0, foundIndex + 1) + "\n" + inputLine.substring(foundIndex + 1);
368                }
369            }
370            output.append(inputLine);
371            output.append("\n");
372        }
373        return output.toString();
374    }
375
376    /**
377     * Given a multi-line input string, this method splits the string so that no line has length greater than
378     * maxLineLength. Any input lines less than or equal to this length remain unaffected.
379     *
380     * @param input the input String.
381     * @param maxLineLength the maximum permitted line length.
382     * @return the split multi-line String.
383     * @throws ArgumentNotValid if input is null or maxLineLength is non-positive
384     */
385    public static String splitStringForce(String input, int maxLineLength) {
386        ArgumentNotValid.checkNotNull(input, "input");
387        ArgumentNotValid.checkPositive(maxLineLength, "maxLineLength");
388        input = input.trim();
389        String[] inputLines = input.split("\n");
390        StringBuffer output = new StringBuffer();
391        for (String inputLine : inputLines) {
392            int lastSplittingIndex = 0;
393            int currentLineLength = inputLine.length();
394            boolean stillSplitting = true;
395            while (stillSplitting) {
396                int nextSplittingIndex = lastSplittingIndex + maxLineLength;
397                if (nextSplittingIndex < currentLineLength - 1) {
398                    output.append(inputLine.substring(lastSplittingIndex, nextSplittingIndex));
399                    output.append("\n");
400                    lastSplittingIndex = nextSplittingIndex;
401                } else {
402                    output.append(inputLine.substring(lastSplittingIndex));
403                    output.append("\n");
404                    stillSplitting = false;
405                }
406            }
407        }
408        return output.toString().trim();
409    }
410
411}