1 package be
.nikiroo
.utils
;
4 import java
.awt
.geom
.AffineTransform
;
5 import java
.awt
.image
.AffineTransformOp
;
6 import java
.awt
.image
.BufferedImage
;
7 import java
.io
.ByteArrayInputStream
;
8 import java
.io
.ByteArrayOutputStream
;
10 import java
.io
.IOException
;
11 import java
.io
.InputStream
;
12 import java
.security
.MessageDigest
;
13 import java
.security
.NoSuchAlgorithmException
;
14 import java
.text
.Normalizer
;
15 import java
.text
.Normalizer
.Form
;
16 import java
.text
.ParseException
;
17 import java
.text
.SimpleDateFormat
;
18 import java
.util
.Base64
;
19 import java
.util
.Date
;
20 import java
.util
.regex
.Pattern
;
22 import javax
.imageio
.ImageIO
;
24 import org
.unbescape
.html
.HtmlEscape
;
25 import org
.unbescape
.html
.HtmlEscapeLevel
;
26 import org
.unbescape
.html
.HtmlEscapeType
;
29 * This class offer some utilities based around {@link String}s.
33 public class StringUtils
{
35 * This enum type will decide the alignment of a {@link String} when padding
36 * is applied or if there is enough horizontal space for it to be aligned.
38 public enum Alignment
{
39 /** Aligned at left. */
43 /** Aligned at right. */
47 static private Pattern marks
= Pattern
48 .compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");
51 * Fix the size of the given {@link String} either with space-padding or by
55 * the {@link String} to fix
57 * the size of the resulting {@link String} or -1 for a noop
59 * @return the resulting {@link String} of size <i>size</i>
61 static public String
padString(String text
, int width
) {
62 return padString(text
, width
, true, Alignment
.Beginning
);
66 * Fix the size of the given {@link String} either with space-padding or by
67 * optionally shortening it.
70 * the {@link String} to fix
72 * the size of the resulting {@link String} if the text fits or
73 * if cut is TRUE or -1 for a noop
75 * cut the {@link String} shorter if needed
77 * align the {@link String} in this position if we have enough
80 * @return the resulting {@link String} of size <i>size</i> minimum
82 static public String
padString(String text
, int width
, boolean cut
,
89 int diff
= width
- text
.length();
93 text
= text
.substring(0, width
);
94 } else if (diff
> 0) {
95 if (diff
< 2 && align
!= Alignment
.End
)
96 align
= Alignment
.Beginning
;
100 text
= text
+ new String(new char[diff
]).replace('\0', ' ');
103 text
= new String(new char[diff
]).replace('\0', ' ') + text
;
107 int pad1
= (diff
) / 2;
108 int pad2
= (diff
+ 1) / 2;
109 text
= new String(new char[pad1
]).replace('\0', ' ') + text
110 + new String(new char[pad2
]).replace('\0', ' ');
120 * Sanitise the given input to make it more Terminal-friendly by removing
121 * combining characters.
124 * the input to sanitise
125 * @param allowUnicode
126 * allow Unicode or only allow ASCII Latin characters
128 * @return the sanitised {@link String}
130 static public String
sanitize(String input
, boolean allowUnicode
) {
131 return sanitize(input
, allowUnicode
, !allowUnicode
);
135 * Sanitise the given input to make it more Terminal-friendly by removing
136 * combining characters.
139 * the input to sanitise
140 * @param allowUnicode
141 * allow Unicode or only allow ASCII Latin characters
142 * @param removeAllAccents
143 * TRUE to replace all accentuated characters by their non
144 * accentuated counter-parts
146 * @return the sanitised {@link String}
148 static public String
sanitize(String input
, boolean allowUnicode
,
149 boolean removeAllAccents
) {
151 if (removeAllAccents
) {
152 input
= Normalizer
.normalize(input
, Form
.NFKD
);
153 input
= marks
.matcher(input
).replaceAll("");
156 input
= Normalizer
.normalize(input
, Form
.NFKC
);
159 StringBuilder builder
= new StringBuilder();
160 for (int index
= 0; index
< input
.length(); index
++) {
161 char car
= input
.charAt(index
);
162 // displayable chars in ASCII are in the range 32<->255,
164 if (car
>= 32 && car
<= 255 && car
!= 127) {
168 input
= builder
.toString();
175 * Convert between time in milliseconds to {@link String} in a "static" way
176 * (to exchange data over the wire, for instance).
179 * the time in milliseconds
181 * @return the time as a {@link String}
183 static public String
fromTime(long time
) {
184 SimpleDateFormat sdf
= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
185 return sdf
.format(new Date(time
));
189 * Convert between time as a {@link String} to milliseconds in a "static"
190 * way (to exchange data over the wire, for instance).
193 * the time as a {@link String}
195 * @return the time in milliseconds
197 static public long toTime(String display
) {
198 SimpleDateFormat sdf
= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
200 return sdf
.parse(display
).getTime();
201 } catch (ParseException e
) {
207 * Convert the given {@link Image} object into a Base64 representation of
208 * the same {@link Image}. object.
211 * the {@link Image} object to convert
213 * @return the Base64 representation
215 * @throws IOException
216 * in case of IO error
218 static public String
fromImage(BufferedImage image
) throws IOException
{
219 String imageString
= null;
220 ByteArrayOutputStream out
= new ByteArrayOutputStream();
222 ImageIO
.write(image
, "jpeg", out
);
223 byte[] imageBytes
= out
.toByteArray();
225 imageString
= new String(Base64
.getEncoder().encode(imageBytes
));
233 * Convert the given {@link File} image into a Base64 representation of the
237 * the {@link File} image to convert
239 * @return the Base64 representation
241 * @throws IOException
242 * in case of IO error
244 static public String
fromStream(InputStream in
) throws IOException
{
245 String fileString
= null;
246 ByteArrayOutputStream out
= new ByteArrayOutputStream();
248 byte[] buf
= new byte[8192];
251 while ((c
= in
.read(buf
, 0, buf
.length
)) > 0) {
252 out
.write(buf
, 0, c
);
257 fileString
= new String(Base64
.getEncoder().encode(out
.toByteArray()));
264 * Convert the given Base64 representation of an image into an {@link Image}
268 * the {@link Image} in Base64 format
270 * @return the {@link Image} object
272 * @throws IOException
273 * in case of IO error
275 static public BufferedImage
toImage(String b64data
) throws IOException
{
276 ByteArrayInputStream in
= new ByteArrayInputStream(Base64
.getDecoder()
282 * Convert the given {@link InputStream} (which must allow calls to
283 * {@link InputStream#reset()}) into an {@link Image} object.
286 * the 'resetable' {@link InputStream}
288 * @return the {@link Image} object
290 * @throws IOException
291 * in case of IO error
293 static public BufferedImage
toImage(InputStream in
) throws IOException
{
296 orientation
= getExifTransorm(in
);
297 } catch (Exception e
) {
298 // no EXIF transform, ok
303 BufferedImage image
= ImageIO
.read(in
);
306 throw new IOException("Failed to convert input to image");
309 // Note: this code has been found on internet;
310 // thank you anonymous coder.
311 int width
= image
.getWidth();
312 int height
= image
.getHeight();
313 AffineTransform affineTransform
= new AffineTransform();
315 switch (orientation
) {
319 affineTransform
.scale(-1.0, 1.0);
320 affineTransform
.translate(-width
, 0);
322 case 3: // PI rotation
323 affineTransform
.translate(width
, height
);
324 affineTransform
.rotate(Math
.PI
);
327 affineTransform
.scale(1.0, -1.0);
328 affineTransform
.translate(0, -height
);
330 case 5: // - PI/2 and Flip X
331 affineTransform
.rotate(-Math
.PI
/ 2);
332 affineTransform
.scale(-1.0, 1.0);
334 case 6: // -PI/2 and -width
335 affineTransform
.translate(height
, 0);
336 affineTransform
.rotate(Math
.PI
/ 2);
338 case 7: // PI/2 and Flip
339 affineTransform
.scale(-1.0, 1.0);
340 affineTransform
.translate(-height
, 0);
341 affineTransform
.translate(0, width
);
342 affineTransform
.rotate(3 * Math
.PI
/ 2);
345 affineTransform
.translate(0, width
);
346 affineTransform
.rotate(3 * Math
.PI
/ 2);
349 affineTransform
= null;
353 if (affineTransform
!= null) {
354 AffineTransformOp affineTransformOp
= new AffineTransformOp(
355 affineTransform
, AffineTransformOp
.TYPE_BILINEAR
);
357 BufferedImage transformedImage
= new BufferedImage(height
, width
,
359 transformedImage
= affineTransformOp
360 .filter(image
, transformedImage
);
362 image
= transformedImage
;
370 * Return a hash of the given {@link String}.
377 static public String
getHash(String input
) {
379 MessageDigest md
= MessageDigest
.getInstance("MD5");
380 md
.update(input
.getBytes());
381 byte byteData
[] = md
.digest();
383 StringBuffer hexString
= new StringBuffer();
384 for (int i
= 0; i
< byteData
.length
; i
++) {
385 String hex
= Integer
.toHexString(0xff & byteData
[i
]);
386 if (hex
.length() == 1)
387 hexString
.append('0');
388 hexString
.append(hex
);
391 return hexString
.toString();
392 } catch (NoSuchAlgorithmException e
) {
398 * Return the EXIF transformation flag of this image if any.
401 * Note: this code has been found on internet; thank you anonymous coder.
405 * the data {@link InputStream}
407 * @return the transformation flag if any
409 * @throws IOException
410 * in case of IO error
412 static private int getExifTransorm(InputStream in
) throws IOException
{
413 int[] exif_data
= new int[100];
417 /* Read File head, check for JPEG SOI + Exif APP1 */
418 for (int i
= 0; i
< 4; i
++)
419 exif_data
[i
] = in
.read();
421 if (exif_data
[0] != 0xFF || exif_data
[1] != 0xD8
422 || exif_data
[2] != 0xFF || exif_data
[3] != 0xE1)
425 /* Get the marker parameter length count */
426 int length
= (in
.read() << 8 | in
.read());
428 /* Length includes itself, so must be at least 2 */
429 /* Following Exif data length must be at least 6 */
433 /* Read Exif head, check for "Exif" */
434 for (int i
= 0; i
< 6; i
++)
435 exif_data
[i
] = in
.read();
437 if (exif_data
[0] != 0x45 || exif_data
[1] != 0x78
438 || exif_data
[2] != 0x69 || exif_data
[3] != 0x66
439 || exif_data
[4] != 0 || exif_data
[5] != 0)
443 length
= length
> exif_data
.length ? exif_data
.length
: length
;
444 for (int i
= 0; i
< length
; i
++)
445 exif_data
[i
] = in
.read();
448 return -1; /* Length of an IFD entry */
450 /* Discover byte order */
451 if (exif_data
[0] == 0x49 && exif_data
[1] == 0x49)
453 else if (exif_data
[0] == 0x4D && exif_data
[1] == 0x4D)
459 if (is_motorola
== 1) {
460 if (exif_data
[2] != 0)
462 if (exif_data
[3] != 0x2A)
465 if (exif_data
[3] != 0)
467 if (exif_data
[2] != 0x2A)
471 /* Get first IFD offset (offset to IFD0) */
473 if (is_motorola
== 1) {
474 if (exif_data
[4] != 0)
476 if (exif_data
[5] != 0)
478 offset
= exif_data
[6];
480 offset
+= exif_data
[7];
482 if (exif_data
[7] != 0)
484 if (exif_data
[6] != 0)
486 offset
= exif_data
[5];
488 offset
+= exif_data
[4];
490 if (offset
> length
- 2)
491 return -1; /* check end of data segment */
493 /* Get the number of directory entries contained in this IFD */
495 if (is_motorola
== 1) {
496 number_of_tags
= exif_data
[offset
];
497 number_of_tags
<<= 8;
498 number_of_tags
+= exif_data
[offset
+ 1];
500 number_of_tags
= exif_data
[offset
+ 1];
501 number_of_tags
<<= 8;
502 number_of_tags
+= exif_data
[offset
];
504 if (number_of_tags
== 0)
508 /* Search for Orientation Tag in IFD0 */
510 if (offset
> length
- 12)
511 return -1; /* check end of data segment */
514 if (is_motorola
== 1) {
515 tagnum
= exif_data
[offset
];
517 tagnum
+= exif_data
[offset
+ 1];
519 tagnum
= exif_data
[offset
+ 1];
521 tagnum
+= exif_data
[offset
];
523 if (tagnum
== 0x0112)
524 break; /* found Orientation Tag */
525 if (--number_of_tags
== 0)
530 /* Get the Orientation value */
531 if (is_motorola
== 1) {
532 if (exif_data
[offset
+ 8] != 0)
534 set_flag
= exif_data
[offset
+ 9];
536 if (exif_data
[offset
+ 9] != 0)
538 set_flag
= exif_data
[offset
+ 8];
547 * Remove the HTML content from the given input, and un-html-ize the rest.
550 * the HTML-encoded content
552 * @return the HTML-free equivalent content
554 public static String
unhtml(String html
) {
555 StringBuilder builder
= new StringBuilder();
558 for (char car
: html
.toCharArray()) {
561 } else if (car
== '>') {
563 } else if (inTag
<= 0) {
568 return HtmlEscape
.unescapeHtml(builder
.toString());
572 * Escape the given {@link String} so it can be used in XML, as content.
575 * the input {@link String}
577 * @return the escaped {@link String}
579 public static String
xmlEscape(String input
) {
584 return HtmlEscape
.escapeHtml(input
,
585 HtmlEscapeType
.HTML4_NAMED_REFERENCES_DEFAULT_TO_HEXA
,
586 HtmlEscapeLevel
.LEVEL_1_ONLY_MARKUP_SIGNIFICANT
);
590 * Escape the given {@link String} so it can be used in XML, as text content
591 * inside double-quotes.
594 * the input {@link String}
596 * @return the escaped {@link String}
598 public static String
xmlEscapeQuote(String input
) {
603 return HtmlEscape
.escapeHtml(input
,
604 HtmlEscapeType
.HTML4_NAMED_REFERENCES_DEFAULT_TO_HEXA
,
605 HtmlEscapeLevel
.LEVEL_1_ONLY_MARKUP_SIGNIFICANT
);