View Javadoc
1   /*
2    * Licensed under the GPL License. You may not use this file except in compliance with the License.
3    * You may obtain a copy of the License at
4    *
5    *   https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
6    *
7    * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
8    * WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
9    * PURPOSE.
10   */
11  package psiprobe;
12  
13  import com.google.common.base.Strings;
14  
15  import jakarta.servlet.http.HttpServletRequest;
16  import jakarta.servlet.http.HttpServletResponse;
17  
18  import java.io.BufferedInputStream;
19  import java.io.BufferedReader;
20  import java.io.ByteArrayInputStream;
21  import java.io.ByteArrayOutputStream;
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.InputStreamReader;
26  import java.io.OutputStream;
27  import java.io.RandomAccessFile;
28  import java.io.Reader;
29  import java.lang.management.ManagementFactory;
30  import java.nio.charset.Charset;
31  import java.nio.charset.StandardCharsets;
32  import java.nio.file.Files;
33  import java.util.ArrayList;
34  import java.util.List;
35  import java.util.Locale;
36  import java.util.Scanner;
37  import java.util.Set;
38  import java.util.zip.ZipEntry;
39  import java.util.zip.ZipOutputStream;
40  
41  import javax.management.MBeanServer;
42  import javax.management.MalformedObjectNameException;
43  import javax.management.ObjectInstance;
44  import javax.management.ObjectName;
45  
46  import org.codelibs.jhighlight.renderer.Renderer;
47  import org.codelibs.jhighlight.renderer.XhtmlRendererFactory;
48  import org.slf4j.Logger;
49  import org.slf4j.LoggerFactory;
50  
51  import psiprobe.tokenizer.StringTokenizer;
52  import psiprobe.tokenizer.Token;
53  import psiprobe.tokenizer.Tokenizer;
54  import psiprobe.tokenizer.TokenizerSymbol;
55  
56  /**
57   * Misc. static helper methods.
58   */
59  public final class Utils {
60  
61    /** The Constant logger. */
62    private static final Logger logger = LoggerFactory.getLogger(Utils.class);
63  
64    /**
65     * Prevent Instantiation.
66     */
67    private Utils() {
68      // Prevent Instantiation
69    }
70  
71    /**
72     * Reads a file on disk. The method uses default file encoding (see: file.encoding system
73     * property)
74     *
75     * @param file to be read
76     * @param charsetName the charset name
77     *
78     * @return String representation of the file contents
79     *
80     * @throws IOException Signals that an I/O exception has occurred.
81     */
82    public static String readFile(File file, String charsetName) throws IOException {
83      try (InputStream fis = Files.newInputStream(file.toPath())) {
84        return readStream(fis, charsetName);
85      }
86    }
87  
88    /**
89     * Reads strings from the intput stream using the given charset. This method closes the input
90     * stream after it has been consumed.
91     *
92     * <p>
93     * This method uses the system's default charset if the given one is unsupported.
94     * </p>
95     *
96     * @param is the stream from which to read
97     * @param charsetName the charset to use when reading the stream
98     *
99     * @return the contents of the given input stream
100    *
101    * @throws IOException if reading from the stream fails spectacularly
102    */
103   public static String readStream(InputStream is, String charsetName) throws IOException {
104     Charset charset;
105     if (Charset.isSupported(charsetName)) {
106       charset = Charset.forName(charsetName);
107     } else {
108       // use system's default encoding if the passed encoding is unsupported
109       charset = Charset.defaultCharset();
110     }
111 
112     StringBuilder out = new StringBuilder();
113     try (BufferedReader reader = new BufferedReader(new InputStreamReader(is, charset), 4096)) {
114       String line;
115       while ((line = reader.readLine()) != null) {
116         out.append(line).append('\n');
117       }
118     }
119 
120     return out.toString();
121   }
122 
123   /**
124    * Delete.
125    *
126    * @param file the file
127    */
128   public static void delete(File file) {
129     if (file != null && file.exists()) {
130       if (file.isDirectory()) {
131         for (File child : file.listFiles()) {
132           delete(child);
133         }
134       }
135       try {
136         Files.delete(file.toPath());
137       } catch (IOException e) {
138         logger.debug("Cannot delete '{}'", file.getAbsolutePath(), e);
139       }
140     } else {
141       logger.debug("'{}' does not exist", file);
142     }
143   }
144 
145   /**
146    * To int.
147    *
148    * @param num the num
149    * @param defaultValue the default value
150    *
151    * @return the int
152    */
153   public static int toInt(String num, int defaultValue) {
154     if (num != null && !num.contains(" ")) {
155       try (Scanner scanner = new Scanner(num)) {
156         if (scanner.hasNextInt()) {
157           return scanner.nextInt();
158         }
159       }
160     }
161     return defaultValue;
162   }
163 
164   /**
165    * To int hex.
166    *
167    * @param num the num
168    * @param defaultValue the default value
169    *
170    * @return the int
171    */
172   public static int toIntHex(String num, int defaultValue) {
173     if (num != null && !num.contains(" ")) {
174       if (num.startsWith("#")) {
175         num = num.substring(1);
176       }
177       try (Scanner scanner = new Scanner(num)) {
178         if (scanner.hasNextInt()) {
179           return scanner.nextInt(16);
180         }
181       }
182     }
183     return defaultValue;
184   }
185 
186   /**
187    * To long.
188    *
189    * @param num the num
190    * @param defaultValue the default value
191    *
192    * @return the long
193    */
194   public static long toLong(String num, long defaultValue) {
195     if (num != null && !num.contains(" ")) {
196       try (Scanner scanner = new Scanner(num)) {
197         if (scanner.hasNextLong()) {
198           return scanner.nextLong();
199         }
200       }
201     }
202     return defaultValue;
203   }
204 
205   /**
206    * To long.
207    *
208    * @param num the num
209    * @param defaultValue the default value
210    *
211    * @return the long
212    */
213   public static long toLong(Long num, long defaultValue) {
214     return num == null ? defaultValue : num;
215   }
216 
217   /**
218    * To float.
219    *
220    * @param num the num
221    * @param defaultValue the default value
222    *
223    * @return the float
224    */
225   public static float toFloat(String num, float defaultValue) {
226     if (num != null && !num.contains(" ")) {
227       try (Scanner scanner = new Scanner(num)) {
228         if (scanner.hasNextFloat()) {
229           return scanner.nextFloat();
230         }
231       }
232     }
233     return defaultValue;
234   }
235 
236   /**
237    * Gets the jsp encoding.
238    *
239    * @param is the is
240    *
241    * @return the jsp encoding
242    *
243    * @throws IOException Signals that an I/O exception has occurred.
244    */
245   public static String getJspEncoding(InputStream is) throws IOException {
246 
247     String encoding = null;
248     String contentType = null;
249 
250     Tokenizer jspTokenizer = new Tokenizer();
251     jspTokenizer.addSymbol("\n", true);
252     jspTokenizer.addSymbol(" ", true);
253     jspTokenizer.addSymbol("\t", true);
254     jspTokenizer.addSymbol(new TokenizerSymbol("dir", "<%@", "%>", false, false, true, false));
255 
256     StringTokenizer directiveTokenizer = new StringTokenizer();
257     directiveTokenizer.addSymbol("\n", true);
258     directiveTokenizer.addSymbol(" ", true);
259     directiveTokenizer.addSymbol("\t", true);
260     directiveTokenizer.addSymbol("=");
261     directiveTokenizer.addSymbol("\"", "\"", false);
262     directiveTokenizer.addSymbol("'", "'", false);
263 
264     StringTokenizer contentTypeTokenizer = new StringTokenizer();
265     contentTypeTokenizer.addSymbol(" ", true);
266     contentTypeTokenizer.addSymbol(";", true);
267 
268     try (Reader reader = new InputStreamReader(is, StandardCharsets.UTF_8)) {
269       jspTokenizer.setReader(reader);
270       while (jspTokenizer.hasMore()) {
271         Token token = jspTokenizer.nextToken();
272         if ("dir".equals(token.getName())) {
273           directiveTokenizer.setString(token.getInnerText());
274           if (directiveTokenizer.hasMore()
275               && "page".equals(directiveTokenizer.nextToken().getText())) {
276             while (directiveTokenizer.hasMore()) {
277               Token directiveToken = directiveTokenizer.nextToken();
278               if ("pageEncoding".equals(directiveToken.getText())) {
279                 if (directiveTokenizer.hasMore()
280                     && "=".equals(directiveTokenizer.nextToken().getText())
281                     && directiveTokenizer.hasMore()) {
282                   encoding = directiveTokenizer.nextToken().getInnerText();
283                   break;
284                 }
285               } else if ("contentType".equals(directiveToken.getText())
286                   && directiveTokenizer.hasMore()
287                   && "=".equals(directiveTokenizer.nextToken().getText())
288                   && directiveTokenizer.hasMore()) {
289                 contentType = directiveTokenizer.nextToken().getInnerText();
290               }
291             }
292           }
293         }
294       }
295     }
296 
297     if (encoding == null && contentType != null) {
298       contentTypeTokenizer.setString(contentType);
299       while (contentTypeTokenizer.hasMore()) {
300         String token = contentTypeTokenizer.nextToken().getText();
301         if (token.startsWith("charset=")) {
302           encoding = token.substring("charset=".length());
303           break;
304         }
305       }
306     }
307 
308     return encoding != null ? encoding : StandardCharsets.UTF_8.name();
309   }
310 
311   /**
312    * Send file.
313    *
314    * @param request the request
315    * @param response the response
316    * @param file the file
317    *
318    * @throws IOException Signals that an I/O exception has occurred.
319    */
320   public static void sendFile(HttpServletRequest request, HttpServletResponse response, File file)
321       throws IOException {
322 
323     try (OutputStream out = response.getOutputStream();
324         RandomAccessFile raf = new RandomAccessFile(file, "r")) {
325       long fileSize = raf.length();
326       long rangeStart = 0;
327       long rangeFinish = fileSize - 1;
328 
329       // accept attempts to resume download (if any)
330       String range = request.getHeader("Range");
331       if (range != null && range.startsWith("bytes=")) {
332         String pureRange = range.replace("bytes=", "");
333         int rangeSep = pureRange.indexOf('-');
334 
335         try {
336           rangeStart = Long.parseLong(pureRange.substring(0, rangeSep));
337           if (rangeStart > fileSize || rangeStart < 0) {
338             rangeStart = 0;
339           }
340         } catch (NumberFormatException e) {
341           // keep rangeStart unchanged
342           logger.trace("", e);
343         }
344 
345         if (rangeSep < pureRange.length() - 1) {
346           try {
347             rangeFinish = Long.parseLong(pureRange.substring(rangeSep + 1));
348             if (rangeFinish < 0 || rangeFinish >= fileSize) {
349               rangeFinish = fileSize - 1;
350             }
351           } catch (NumberFormatException e) {
352             logger.trace("", e);
353           }
354         }
355       }
356 
357       // set some headers
358       response.setContentType("application/x-download");
359       response.setHeader("Content-Disposition", "attachment; filename=" + file.getName());
360       response.setHeader("Accept-Ranges", "bytes");
361       response.setHeader("Content-Length", Long.toString(rangeFinish - rangeStart + 1));
362       response.setHeader("Content-Range",
363           "bytes " + rangeStart + "-" + rangeFinish + "/" + fileSize);
364 
365       // seek to the requested offset
366       raf.seek(rangeStart);
367 
368       // send the file
369       byte[] buffer = new byte[4096];
370 
371       long len;
372       int totalRead = 0;
373       boolean nomore = false;
374       while (true) {
375         len = raf.read(buffer);
376         if (len > 0 && totalRead + len > rangeFinish - rangeStart + 1) {
377           // read more then required?
378           // adjust the length
379           len = rangeFinish - rangeStart + 1 - totalRead;
380           nomore = true;
381         }
382 
383         if (len <= 0) {
384           break;
385         }
386         out.write(buffer, 0, (int) len);
387         totalRead = totalRead + (int) len;
388         if (nomore) {
389           break;
390         }
391       }
392     }
393   }
394 
395   /**
396    * Gets the thread by name.
397    *
398    * @param name the name
399    *
400    * @return the thread by name
401    */
402   public static Thread getThreadByName(String name) {
403     if (name != null) {
404       // get top ThreadGroup
405       ThreadGroup masterGroup = Thread.currentThread().getThreadGroup();
406       while (masterGroup.getParent() != null) {
407         masterGroup = masterGroup.getParent();
408       }
409 
410       Thread[] threads = new Thread[masterGroup.activeCount()];
411       masterGroup.enumerate(threads);
412 
413       for (Thread thread : threads) {
414         if (thread != null && name.equals(thread.getName())) {
415           return thread;
416         }
417       }
418     }
419     return null;
420   }
421 
422   /**
423    * Highlight stream.
424    *
425    * @param name the name
426    * @param input the input
427    * @param rendererName the renderer name
428    * @param encoding the encoding
429    *
430    * @return the string
431    *
432    * @throws IOException Signals that an I/O exception has occurred.
433    */
434   public static String highlightStream(String name, InputStream input, String rendererName,
435       String encoding) throws IOException {
436 
437     Renderer jspRenderer = XhtmlRendererFactory.getRenderer(rendererName);
438     if (jspRenderer == null) {
439       return null;
440     }
441 
442     ByteArrayOutputStream bos = new ByteArrayOutputStream();
443     jspRenderer.highlight(name, input, bos, encoding, true);
444 
445     ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
446 
447     Tokenizer tokenizer = new Tokenizer(new InputStreamReader(bis, Charset.forName(encoding)));
448     tokenizer.addSymbol(new TokenizerSymbol("EOL", "\n", null, false, false, true, false));
449     tokenizer.addSymbol(new TokenizerSymbol("EOL", "\r\n", null, false, false, true, false));
450 
451     //
452     // JHighlight adds HTML comment as the first line, so if
453     // we number the lines we could end up with a line number and no line
454     // to avoid that we just ignore the first line all together.
455     //
456     StringBuilder buffer = new StringBuilder();
457     long counter = 0;
458     while (tokenizer.hasMore()) {
459       Token tk = tokenizer.nextToken();
460       if ("EOL".equals(tk.getName())) {
461         counter++;
462         buffer.append(tk.getText());
463       } else if (counter > 0) {
464         buffer.append("<span class=\"codeline\">");
465         buffer.append("<span class=\"linenum\">");
466         buffer.append(leftPad(Long.toString(counter), 6, " ").replace(" ", "&nbsp;"));
467         buffer.append("</span>");
468         buffer.append(tk.getText());
469         buffer.append("</span>");
470       }
471     }
472     return buffer.toString();
473   }
474 
475   /**
476    * Send compressed file.
477    *
478    * @param response the response
479    * @param file the file
480    *
481    * @throws IOException Signals that an I/O exception has occurred.
482    */
483   public static void sendCompressedFile(HttpServletResponse response, File file)
484       throws IOException {
485     try (ZipOutputStream zip = new ZipOutputStream(response.getOutputStream());
486         InputStream fileInput = new BufferedInputStream(Files.newInputStream(file.toPath()))) {
487 
488       String fileName = file.getName();
489 
490       // set some headers
491       response.setContentType("application/zip");
492       response.setHeader("Content-Disposition", "attachment; filename=" + fileName + ".zip");
493 
494       zip.putNextEntry(new ZipEntry(fileName));
495 
496       // send the file
497       byte[] buffer = new byte[4096];
498       long len;
499 
500       while ((len = fileInput.read(buffer)) > 0) {
501         zip.write(buffer, 0, (int) len);
502       }
503       zip.closeEntry();
504     }
505   }
506 
507   /**
508    * Left pad.
509    *
510    * @param str the str
511    * @param len the len
512    * @param fill the fill
513    *
514    * @return the string
515    */
516   static String leftPad(final String str, final int len, final String fill) {
517     if (str != null && str.length() < len) {
518       return Strings.padStart(str, len, fill.charAt(0));
519     }
520     return str == null ? "" : str;
521   }
522 
523   /**
524    * Gets the names for locale.
525    *
526    * @param baseName the base name
527    * @param locale the locale
528    *
529    * @return the names for locale
530    */
531   public static List<String> getNamesForLocale(String baseName, Locale locale) {
532     List<String> result = new ArrayList<>(3);
533     String language = locale.getLanguage();
534     String country = locale.getCountry();
535     String variant = locale.getVariant();
536     StringBuilder temp = new StringBuilder(baseName);
537 
538     if (language.length() > 0) {
539       temp.append('_').append(language);
540       result.add(0, temp.toString());
541     }
542 
543     if (country.length() > 0) {
544       temp.append('_').append(country);
545       result.add(0, temp.toString());
546     }
547 
548     if (variant.length() > 0) {
549       temp.append('_').append(variant);
550       result.add(0, temp.toString());
551     }
552 
553     return result;
554   }
555 
556   /**
557    * Checks if is threading enabled.
558    *
559    * @return true, if is threading enabled
560    */
561   public static boolean isThreadingEnabled() {
562     try {
563       MBeanServer mbeanServer = ManagementFactory.getPlatformMBeanServer();
564       ObjectName objectNameThreading = new ObjectName("java.lang:type=Threading");
565       Set<ObjectInstance> threading = mbeanServer.queryMBeans(objectNameThreading, null);
566       return threading != null && !threading.isEmpty();
567     } catch (MalformedObjectNameException e) {
568       logger.trace("", e);
569       return false;
570     }
571   }
572 }