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
.Date
;
19 import java
.util
.regex
.Pattern
;
21 import javax
.imageio
.ImageIO
;
23 import org
.unbescape
.html
.HtmlEscape
;
24 import org
.unbescape
.html
.HtmlEscapeLevel
;
25 import org
.unbescape
.html
.HtmlEscapeType
;
28 * This class offer some utilities based around {@link String}s.
32 public class StringUtils
{
34 * This enum type will decide the alignment of a {@link String} when padding
35 * is applied or if there is enough horizontal space for it to be aligned.
37 public enum Alignment
{
38 /** Aligned at left. */
42 /** Aligned at right. */
46 static private Pattern marks
= Pattern
47 .compile("[\\p{InCombiningDiacriticalMarks}\\p{IsLm}\\p{IsSk}]+");
50 * Fix the size of the given {@link String} either with space-padding or by
54 * the {@link String} to fix
56 * the size of the resulting {@link String} or -1 for a noop
58 * @return the resulting {@link String} of size <i>size</i>
60 static public String
padString(String text
, int width
) {
61 return padString(text
, width
, true, Alignment
.Beginning
);
65 * Fix the size of the given {@link String} either with space-padding or by
66 * optionally shortening it.
69 * the {@link String} to fix
71 * the size of the resulting {@link String} if the text fits or
72 * if cut is TRUE or -1 for a noop
74 * cut the {@link String} shorter if needed
76 * align the {@link String} in this position if we have enough
79 * @return the resulting {@link String} of size <i>size</i> minimum
81 static public String
padString(String text
, int width
, boolean cut
,
88 int diff
= width
- text
.length();
92 text
= text
.substring(0, width
);
93 } else if (diff
> 0) {
94 if (diff
< 2 && align
!= Alignment
.End
)
95 align
= Alignment
.Beginning
;
99 text
= text
+ new String(new char[diff
]).replace('\0', ' ');
102 text
= new String(new char[diff
]).replace('\0', ' ') + text
;
106 int pad1
= (diff
) / 2;
107 int pad2
= (diff
+ 1) / 2;
108 text
= new String(new char[pad1
]).replace('\0', ' ') + text
109 + new String(new char[pad2
]).replace('\0', ' ');
119 * Sanitise the given input to make it more Terminal-friendly by removing
120 * combining characters.
123 * the input to sanitise
124 * @param allowUnicode
125 * allow Unicode or only allow ASCII Latin characters
127 * @return the sanitised {@link String}
129 static public String
sanitize(String input
, boolean allowUnicode
) {
130 return sanitize(input
, allowUnicode
, !allowUnicode
);
134 * Sanitise the given input to make it more Terminal-friendly by removing
135 * combining characters.
138 * the input to sanitise
139 * @param allowUnicode
140 * allow Unicode or only allow ASCII Latin characters
141 * @param removeAllAccents
142 * TRUE to replace all accentuated characters by their non
143 * accentuated counter-parts
145 * @return the sanitised {@link String}
147 static public String
sanitize(String input
, boolean allowUnicode
,
148 boolean removeAllAccents
) {
150 if (removeAllAccents
) {
151 input
= Normalizer
.normalize(input
, Form
.NFKD
);
152 input
= marks
.matcher(input
).replaceAll("");
155 input
= Normalizer
.normalize(input
, Form
.NFKC
);
158 StringBuilder builder
= new StringBuilder();
159 for (int index
= 0; index
< input
.length(); index
++) {
160 char car
= input
.charAt(index
);
161 // displayable chars in ASCII are in the range 32<->255,
163 if (car
>= 32 && car
<= 255 && car
!= 127) {
167 input
= builder
.toString();
174 * Convert between time in milliseconds to {@link String} in a "static" way
175 * (to exchange data over the wire, for instance).
178 * the time in milliseconds
180 * @return the time as a {@link String}
182 static public String
fromTime(long time
) {
183 SimpleDateFormat sdf
= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
184 return sdf
.format(new Date(time
));
188 * Convert between time as a {@link String} to milliseconds in a "static"
189 * way (to exchange data over the wire, for instance).
192 * the time as a {@link String}
194 * @return the time in milliseconds
196 static public long toTime(String display
) {
197 SimpleDateFormat sdf
= new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
199 return sdf
.parse(display
).getTime();
200 } catch (ParseException e
) {
206 * Convert the given {@link Image} object into a Base64 representation of
207 * the same {@link Image}. object.
210 * the {@link Image} object to convert
212 * @return the Base64 representation
214 * @throws IOException
215 * in case of IO error
217 static public String
fromImage(BufferedImage image
) throws IOException
{
218 String imageString
= null;
219 ByteArrayOutputStream out
= new ByteArrayOutputStream();
221 ImageIO
.write(image
, "jpeg", out
);
222 byte[] imageBytes
= out
.toByteArray();
224 imageString
= new String(Base64
.encodeBytes(imageBytes
));
232 * Convert the given {@link File} image into a Base64 representation of the
236 * the {@link File} image to convert
238 * @return the Base64 representation
240 * @throws IOException
241 * in case of IO error
243 static public String
fromStream(InputStream in
) throws IOException
{
244 String fileString
= null;
245 ByteArrayOutputStream out
= new ByteArrayOutputStream();
247 byte[] buf
= new byte[8192];
250 while ((c
= in
.read(buf
, 0, buf
.length
)) > 0) {
251 out
.write(buf
, 0, c
);
256 fileString
= new String(Base64
.encodeBytes(out
.toByteArray()));
263 * Convert the given Base64 representation of an image into an {@link Image}
267 * the {@link Image} in Base64 format
269 * @return the {@link Image} object
271 * @throws IOException
272 * in case of IO error
274 static public BufferedImage
toImage(String b64data
) throws IOException
{
275 ByteArrayInputStream in
= new ByteArrayInputStream(
276 Base64
.decode(b64data
));
281 * Convert the given {@link InputStream} (which must allow calls to
282 * {@link InputStream#reset()}) into an {@link Image} object.
285 * the 'resetable' {@link InputStream}
287 * @return the {@link Image} object
289 * @throws IOException
290 * in case of IO error
292 static public BufferedImage
toImage(InputStream in
) throws IOException
{
295 orientation
= getExifTransorm(in
);
296 } catch (Exception e
) {
297 // no EXIF transform, ok
302 BufferedImage image
= ImageIO
.read(in
);
305 throw new IOException("Failed to convert input to image");
308 // Note: this code has been found on internet;
309 // thank you anonymous coder.
310 int width
= image
.getWidth();
311 int height
= image
.getHeight();
312 AffineTransform affineTransform
= new AffineTransform();
314 switch (orientation
) {
318 affineTransform
.scale(-1.0, 1.0);
319 affineTransform
.translate(-width
, 0);
321 case 3: // PI rotation
322 affineTransform
.translate(width
, height
);
323 affineTransform
.rotate(Math
.PI
);
326 affineTransform
.scale(1.0, -1.0);
327 affineTransform
.translate(0, -height
);
329 case 5: // - PI/2 and Flip X
330 affineTransform
.rotate(-Math
.PI
/ 2);
331 affineTransform
.scale(-1.0, 1.0);
333 case 6: // -PI/2 and -width
334 affineTransform
.translate(height
, 0);
335 affineTransform
.rotate(Math
.PI
/ 2);
337 case 7: // PI/2 and Flip
338 affineTransform
.scale(-1.0, 1.0);
339 affineTransform
.translate(-height
, 0);
340 affineTransform
.translate(0, width
);
341 affineTransform
.rotate(3 * Math
.PI
/ 2);
344 affineTransform
.translate(0, width
);
345 affineTransform
.rotate(3 * Math
.PI
/ 2);
348 affineTransform
= null;
352 if (affineTransform
!= null) {
353 AffineTransformOp affineTransformOp
= new AffineTransformOp(
354 affineTransform
, AffineTransformOp
.TYPE_BILINEAR
);
356 BufferedImage transformedImage
= new BufferedImage(height
, width
,
358 transformedImage
= affineTransformOp
359 .filter(image
, transformedImage
);
361 image
= transformedImage
;
369 * Return a hash of the given {@link String}.
376 static public String
getHash(String input
) {
378 MessageDigest md
= MessageDigest
.getInstance("MD5");
379 md
.update(input
.getBytes());
380 byte byteData
[] = md
.digest();
382 StringBuffer hexString
= new StringBuffer();
383 for (int i
= 0; i
< byteData
.length
; i
++) {
384 String hex
= Integer
.toHexString(0xff & byteData
[i
]);
385 if (hex
.length() == 1)
386 hexString
.append('0');
387 hexString
.append(hex
);
390 return hexString
.toString();
391 } catch (NoSuchAlgorithmException e
) {
397 * Return the EXIF transformation flag of this image if any.
400 * Note: this code has been found on internet; thank you anonymous coder.
404 * the data {@link InputStream}
406 * @return the transformation flag if any
408 * @throws IOException
409 * in case of IO error
411 static private int getExifTransorm(InputStream in
) throws IOException
{
412 int[] exif_data
= new int[100];
416 /* Read File head, check for JPEG SOI + Exif APP1 */
417 for (int i
= 0; i
< 4; i
++)
418 exif_data
[i
] = in
.read();
420 if (exif_data
[0] != 0xFF || exif_data
[1] != 0xD8
421 || exif_data
[2] != 0xFF || exif_data
[3] != 0xE1)
424 /* Get the marker parameter length count */
425 int length
= (in
.read() << 8 | in
.read());
427 /* Length includes itself, so must be at least 2 */
428 /* Following Exif data length must be at least 6 */
432 /* Read Exif head, check for "Exif" */
433 for (int i
= 0; i
< 6; i
++)
434 exif_data
[i
] = in
.read();
436 if (exif_data
[0] != 0x45 || exif_data
[1] != 0x78
437 || exif_data
[2] != 0x69 || exif_data
[3] != 0x66
438 || exif_data
[4] != 0 || exif_data
[5] != 0)
442 length
= length
> exif_data
.length ? exif_data
.length
: length
;
443 for (int i
= 0; i
< length
; i
++)
444 exif_data
[i
] = in
.read();
447 return -1; /* Length of an IFD entry */
449 /* Discover byte order */
450 if (exif_data
[0] == 0x49 && exif_data
[1] == 0x49)
452 else if (exif_data
[0] == 0x4D && exif_data
[1] == 0x4D)
458 if (is_motorola
== 1) {
459 if (exif_data
[2] != 0)
461 if (exif_data
[3] != 0x2A)
464 if (exif_data
[3] != 0)
466 if (exif_data
[2] != 0x2A)
470 /* Get first IFD offset (offset to IFD0) */
472 if (is_motorola
== 1) {
473 if (exif_data
[4] != 0)
475 if (exif_data
[5] != 0)
477 offset
= exif_data
[6];
479 offset
+= exif_data
[7];
481 if (exif_data
[7] != 0)
483 if (exif_data
[6] != 0)
485 offset
= exif_data
[5];
487 offset
+= exif_data
[4];
489 if (offset
> length
- 2)
490 return -1; /* check end of data segment */
492 /* Get the number of directory entries contained in this IFD */
494 if (is_motorola
== 1) {
495 number_of_tags
= exif_data
[offset
];
496 number_of_tags
<<= 8;
497 number_of_tags
+= exif_data
[offset
+ 1];
499 number_of_tags
= exif_data
[offset
+ 1];
500 number_of_tags
<<= 8;
501 number_of_tags
+= exif_data
[offset
];
503 if (number_of_tags
== 0)
507 /* Search for Orientation Tag in IFD0 */
509 if (offset
> length
- 12)
510 return -1; /* check end of data segment */
513 if (is_motorola
== 1) {
514 tagnum
= exif_data
[offset
];
516 tagnum
+= exif_data
[offset
+ 1];
518 tagnum
= exif_data
[offset
+ 1];
520 tagnum
+= exif_data
[offset
];
522 if (tagnum
== 0x0112)
523 break; /* found Orientation Tag */
524 if (--number_of_tags
== 0)
529 /* Get the Orientation value */
530 if (is_motorola
== 1) {
531 if (exif_data
[offset
+ 8] != 0)
533 set_flag
= exif_data
[offset
+ 9];
535 if (exif_data
[offset
+ 9] != 0)
537 set_flag
= exif_data
[offset
+ 8];
546 * Remove the HTML content from the given input, and un-html-ize the rest.
549 * the HTML-encoded content
551 * @return the HTML-free equivalent content
553 public static String
unhtml(String html
) {
554 StringBuilder builder
= new StringBuilder();
557 for (char car
: html
.toCharArray()) {
560 } else if (car
== '>') {
562 } else if (inTag
<= 0) {
567 return HtmlEscape
.unescapeHtml(builder
.toString());
571 * Escape the given {@link String} so it can be used in XML, as content.
574 * the input {@link String}
576 * @return the escaped {@link String}
578 public static String
xmlEscape(String input
) {
583 return HtmlEscape
.escapeHtml(input
,
584 HtmlEscapeType
.HTML4_NAMED_REFERENCES_DEFAULT_TO_HEXA
,
585 HtmlEscapeLevel
.LEVEL_1_ONLY_MARKUP_SIGNIFICANT
);
589 * Escape the given {@link String} so it can be used in XML, as text content
590 * inside double-quotes.
593 * the input {@link String}
595 * @return the escaped {@link String}
597 public static String
xmlEscapeQuote(String input
) {
602 return HtmlEscape
.escapeHtml(input
,
603 HtmlEscapeType
.HTML4_NAMED_REFERENCES_DEFAULT_TO_HEXA
,
604 HtmlEscapeLevel
.LEVEL_1_ONLY_MARKUP_SIGNIFICANT
);