Java tutorial
/* * Copyright 2013-2015 Erudika. http://erudika.com * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * For issues and patches go to: https://github.com/erudika */ package com.erudika.para.utils; import com.erudika.para.annotations.Email; import com.erudika.para.core.ParaObject; import com.github.mustachejava.DefaultMustacheFactory; import com.github.mustachejava.MustacheFactory; import com.github.rjeschke.txtmark.Configuration; import com.github.rjeschke.txtmark.Processor; import java.io.IOException; import java.io.StringReader; import java.io.StringWriter; import java.io.UnsupportedEncodingException; import java.io.Writer; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.math.BigDecimal; import java.math.RoundingMode; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.net.URL; import java.net.URLDecoder; import java.net.URLEncoder; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; import java.text.DateFormatSymbols; import java.text.MessageFormat; import java.text.NumberFormat; import java.util.Calendar; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Random; import java.util.regex.Pattern; import javax.servlet.http.Cookie; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.apache.commons.codec.binary.Base64; import org.apache.commons.lang3.StringEscapeUtils; import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.math.NumberUtils; import org.apache.commons.lang3.time.DateFormatUtils; import org.jsoup.Jsoup; import org.mindrot.jbcrypt.BCrypt; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * Misc. Para utilities. * @author Alex Bogdanovski [alex@erudika.com] */ @SuppressWarnings("unchecked") public final class Utils { private static final Logger logger = LoggerFactory.getLogger(Utils.class); // maps lowercase simple names to class objects private static final Pattern emailz = Pattern.compile(Email.EMAIL_PATTERN); private static final NumberFormat numberFormat = NumberFormat.getInstance(); private static final MustacheFactory mustache; private static HumanTime humantime; private static Utils instance; ////////// ID GEN VARS ////////////// private static final long TIMER_OFFSET = 1310084584692L; // ~July 2011 private static final long workerIdBits = 5L; private static final long dataCenterIdBits = 5L; private static final long maxWorkerId = -1L ^ (-1L << workerIdBits); private static final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits); private static final long sequenceBits = 12L; private static final long workerIdShift = sequenceBits; private static final long dataCenterIdShift = sequenceBits + workerIdBits; private static final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits; private static final long sequenceMask = -1L ^ (-1L << sequenceBits); private static long lastTimestamp = -1L; private static long dataCenterId = 0L; // only one datacenter atm private static long workerId; // max 1024 private static long sequence = 0L; static { initIdGenerator(); mustache = new DefaultMustacheFactory(); numberFormat.setMinimumFractionDigits(2); numberFormat.setMaximumFractionDigits(2); } private Utils() { } /** * Returns an instance of this class. * @return an instance */ public static Utils getInstance() { if (instance == null) { instance = new Utils(); } return instance; } /** * HumanTime - a relative time formatter * @return humantime instance */ public static HumanTime getHumanTime() { if (humantime == null) { humantime = new HumanTime(); } return humantime; } ///////////////////////////////////////////// // INIT FUNCTIONS ///////////////////////////////////////////// private static void initIdGenerator() { String workerID = Config.WORKER_ID; workerId = NumberUtils.toLong(workerID, 1); if (workerId > maxWorkerId || workerId < 0) { workerId = new Random().nextInt((int) maxWorkerId + 1); } if (dataCenterId > maxDataCenterId || dataCenterId < 0) { dataCenterId = new Random().nextInt((int) maxDataCenterId + 1); } } ///////////////////////////////////////////// // HASH UTILS ///////////////////////////////////////////// /** * MD5 hash function * @param s the string to be hashed * @return an md5 hash */ public static String MD5(String s) { if (s == null) { return ""; } try { MessageDigest md = MessageDigest.getInstance("MD5"); md.update(s.getBytes()); byte byteData[] = md.digest(); //convert the byte to hex format method 1 StringBuilder sb = new StringBuilder(); for (int i = 0; i < byteData.length; i++) { sb.append(Integer.toString((byteData[i] & 0xff) + 0x100, 16).substring(1)); } return sb.toString(); } catch (NoSuchAlgorithmException ex) { return ""; } } /** * bcrypt hash function implemented by Spring Security * * @param s the string to be hashed * @return the hash */ public static String bcrypt(String s) { return (s == null) ? s : BCrypt.hashpw(s, BCrypt.gensalt(12)); } /** * Checks if a hash matches a string. * * @param plain plain text string * @param storedHash hashed string * @return true if the hash matches */ public static boolean bcryptMatches(String plain, String storedHash) { if (StringUtils.isBlank(plain) || StringUtils.isBlank(storedHash)) { return false; } try { return BCrypt.checkpw(plain, storedHash); } catch (Exception e) { return false; } } /** * Generates an authentication token - a random string encoded in Base64. * @param length the length of the generated token * @param urlSafe switches to a URL safe encoding * @return a random string */ public static String generateSecurityToken(int length, boolean urlSafe) { final byte[] bytes = new byte[length]; SecureRandom rand; try { rand = SecureRandom.getInstance("SHA1PRNG"); } catch (NoSuchAlgorithmException ex) { logger.error(null, ex); rand = new SecureRandom(); } rand.nextBytes(bytes); return urlSafe ? base64encURL(bytes) : base64enc(bytes); } /** * Generates an authentication token - a random string encoded in Base64. * @param length the length of the generated token * @return a random string */ public static String generateSecurityToken(int length) { return generateSecurityToken(length, false); } /** * Generates an authentication token - a random 32 byte string encoded in Base64. * @return a random string */ public static String generateSecurityToken() { return generateSecurityToken(32); } ///////////////////////////////////////////// // STRING UTILS ///////////////////////////////////////////// /** * Escapes JavaScript * @param str a javascript string * @return the escaped javascript string */ public static String escapeJavascript(String str) { return (str == null) ? "" : StringEscapeUtils.escapeEcmaScript(str); } /** * Strips all HTML tags from a string * @param html HTML string * @return just the text */ public static String stripHtml(String html) { return (html == null) ? "" : Jsoup.parse(html).text(); } /** * Converts Markdown to HTML * @param markdownString Markdown * @return HTML */ public static String markdownToHtml(String markdownString) { return StringUtils.isBlank(markdownString) ? "" : Processor.process(markdownString, Configuration.DEFAULT_SAFE); } /** * Compiles a mustache template with a given scope (map of fields and values). * @param scope a map of fields and values * @param template a Mustache template * @return the compiled template string */ public static String compileMustache(Map<String, Object> scope, String template) { if (scope == null || StringUtils.isBlank(template)) { return ""; } Writer writer = new StringWriter(); try { mustache.compile(new StringReader(template), MD5(template)).execute(writer, scope); } finally { try { writer.close(); } catch (IOException e) { logger.error(null, e); } } return writer.toString(); } /** * Abbreviates a string * @param str a string * @param max max length * @return a substring of that string */ public static String abbreviate(String str, int max) { return StringUtils.isBlank(str) ? "" : StringUtils.abbreviate(str, max); } /** * Joins a list of strings to String using a separator. * @param arr a list of strings * @param separator a separator string * @return a string */ public static String arrayJoin(List<String> arr, String separator) { return (arr == null || separator == null) ? "" : StringUtils.join(arr, separator); } /** * Strips all symbols, punctuation, whitespace and control chars from a string. * @param str a dirty string * @return a clean string */ public static String stripAndTrim(String str) { return stripAndTrim(str, ""); } /** * Strips all symbols, punctuation, whitespace and control chars from a string. * @param str a dirty string * @param replaceWith a string to replace spaces with * @return a clean string */ public static String stripAndTrim(String str, String replaceWith) { return StringUtils.isBlank(str) ? "" : str.replaceAll("[\\p{S}\\p{P}\\p{C}]", replaceWith).replaceAll("\\p{Z}+", " ").trim(); } /** * Converts spaces to dashes. * @param str a string with spaces * @param replaceWith a string to replace spaces with * @return a string with dashes */ public static String noSpaces(String str, String replaceWith) { return StringUtils.isBlank(str) ? "" : str.trim().replaceAll("[\\p{C}\\p{Z}]+", StringUtils.trimToEmpty(replaceWith)).toLowerCase(); } /** * Formats a messages containing {0}, {1}... etc. Used for translation. * @param msg a message with placeholders * @param params objects used to populate the placeholders * @return a formatted message */ public static String formatMessage(String msg, Object... params) { try { // required by MessageFormat, single quotes break string interpolation! msg = StringUtils.replace(msg, "'", "''"); return StringUtils.isBlank(msg) ? "" : MessageFormat.format(msg, params); } catch (IllegalArgumentException e) { return msg; } } /** * Encodes a byte array to Base64 * @param str the byte array * @return an encoded string */ public static String base64enc(byte[] str) { if (str == null) { return ""; } return new String(Base64.encodeBase64(str)); } /** * Encodes a byte array to Base64. URL safe. * @param str the byte array * @return an encoded string */ public static String base64encURL(byte[] str) { if (str == null) { return ""; } return new String(Base64.encodeBase64URLSafe(str)); } /** * Decodes a string from Base64 * @param str the encoded string * @return a decoded string */ public static String base64dec(String str) { if (str == null) { return ""; } try { return new String(Base64.decodeBase64(str), Config.DEFAULT_ENCODING); } catch (UnsupportedEncodingException ex) { return ""; } } ///////////////////////////////////////////// // DATE UTILS ///////////////////////////////////////////// /** * Formats a date in a specific format. * @param timestamp the Java timestamp * @param format the date format * @param loc the locale instance * @return a formatted date */ public static String formatDate(Long timestamp, String format, Locale loc) { if (StringUtils.isBlank(format)) { format = DateFormatUtils.ISO_DATE_FORMAT.getPattern(); } if (timestamp == null) { timestamp = timestamp(); } if (loc == null) { loc = Locale.US; } return DateFormatUtils.format(timestamp, format, loc); } /** * Formats the date for today, in a specific format. * @param format the date format * @param loc the locale instance * @return today's date formatted */ public static String formatDate(String format, Locale loc) { return formatDate(timestamp(), format, loc); } /** * Returns the current year. * @return this year */ public static int getCurrentYear() { return Calendar.getInstance().get(Calendar.YEAR); } /** * Java timestamp * @return {@link java.lang.System#currentTimeMillis()} */ public static long timestamp() { return System.currentTimeMillis(); } /** * Returns an array of the months in the Gregorian calendar. * @param locale the locale used for the months' names * @return an array of the 12 months */ public static String[] getMonths(Locale locale) { if (locale == null) { locale = Locale.US; } DateFormatSymbols dfs = DateFormatSymbols.getInstance(locale); return dfs.getMonths(); } ///////////////////////////////////////////// // NUMBER UTILS ///////////////////////////////////////////// /** * Rounds a float to an int * @param d a float * @return a rounded int */ public static int round(float d) { return Math.round(d); } /** * Returns the price with two fractional digits at the end * @param price a price * @return $###.## */ public static String formatPrice(double price) { return numberFormat.format(price); } /** * Round up a double using the "half up" method * @param d a double * @return a double */ public static double roundHalfUp(double d) { return roundHalfUp(d, 2); } /** * Round up a double using the "half up" method * @param d a double * @param scale the scale * @return a double */ public static double roundHalfUp(double d, int scale) { return BigDecimal.valueOf(d).setScale(scale, RoundingMode.HALF_UP).doubleValue(); } /** * Abbreviates an integer by adding a letter suffix at the end. * E.g. "M" for millions, "K" for thousands, etc. * @param number a big integer * @param decPlaces decimal places * @return the rounded integer as a string */ public static String abbreviateInt(Number number, int decPlaces) { if (number == null) { return ""; } String abbrevn = number.toString(); // 2 decimal places => 100, 3 => 1000, etc decPlaces = (int) Math.pow(10, decPlaces); // Enumerate number abbreviations String[] abbrev = { "K", "M", "B", "T" }; boolean done = false; // Go through the array backwards, so we do the largest first for (int i = abbrev.length - 1; i >= 0 && !done; i--) { // Convert array index to "1000", "1000000", etc int size = (int) Math.pow(10, (i + 1) * 3); // If the number is bigger or equal do the abbreviation if (size <= number.intValue()) { // Here, we multiply by decPlaces, round, and then divide by decPlaces. // This gives us nice rounding to a particular decimal place. number = Math.round(number.intValue() * decPlaces / size) / decPlaces; // Add the letter for the abbreviation abbrevn = number + abbrev[i]; // We are done... stop done = true; } } return abbrevn; } ///////////////////////////////////////////// // URL UTILS ///////////////////////////////////////////// /** * Decodes a URL-encoded string * @param s a string * @return the decoded string */ public static String urlDecode(String s) { if (s == null) { return ""; } String decoded = s; try { decoded = URLDecoder.decode(s, Config.DEFAULT_ENCODING); } catch (UnsupportedEncodingException ex) { logger.error(null, ex); } return decoded; } /** * URL-encodes a string * @param s a string * @return the encoded string */ public static String urlEncode(String s) { if (s == null) { return ""; } String encoded = s; try { encoded = URLEncoder.encode(s, Config.DEFAULT_ENCODING); } catch (UnsupportedEncodingException ex) { logger.error(null, ex); } return encoded; } /** * URL validation * @param url a URL * @return true if the URL is valid */ public static boolean isValidURL(String url) { return toURL(url) != null; } /** * Email validation * @param url a URL * @return true if the URL is valid */ public static boolean isValidEmail(String url) { return emailz.matcher(url).matches(); } /** * Returns the host part of the URL * @param url a URL * @return just the host */ public static String getHostFromURL(String url) { URL u = toURL(url); String host = (u == null) ? "" : u.getHost(); return host; } /** * The basic URL without any parameters: >scheme<:>authority< * @param url a full URL * @return the basic URL */ public static String getBaseURL(String url) { URL u = toURL(url); String base = null; if (u != null) { try { base = u.toURI().getScheme().concat("://").concat(u.getAuthority()); } catch (URISyntaxException ex) { base = null; } } return base; } private static URL toURL(String url) { if (StringUtils.isBlank(url)) { return null; } URL u; try { u = new URL(url); } catch (MalformedURLException e) { // the URL is not in a valid form u = null; } return u; } /** * Returns the default URL for a given domain object. * @param obj the domain object * @param includeName true if we want to include the name of the object in the URL * @param includeId true if we want to include the ID of the object in the URL * @return the object's URL - e.g. /users/123-name, /users/, /users/123 */ public static String getObjectURI(ParaObject obj, boolean includeName, boolean includeId) { if (obj == null) { return "/"; } if (includeId && obj.getId() != null) { return (includeName && !StringUtils.isBlank(obj.getName())) ? obj.getObjectURI().concat("-").concat(urlEncode(noSpaces(obj.getName(), "-"))) : obj.getObjectURI(); } else { return obj.getObjectURI(); } } ///////////////////////////////////////////// // COOKIE & STATE UTILS ///////////////////////////////////////////// /** * Sets a cookie. * @param name the name * @param value the value * @param req HTTP request * @param res HTTP response */ public static void setStateParam(String name, String value, HttpServletRequest req, HttpServletResponse res) { setStateParam(name, value, req, res, false); } /** * Sets a cookie. * @param name the name * @param value the value * @param req HTTP request * @param res HTTP response * @param httpOnly HTTP only flag */ public static void setStateParam(String name, String value, HttpServletRequest req, HttpServletResponse res, boolean httpOnly) { setRawCookie(name, value, req, res, httpOnly, -1); } /** * Reads a cookie. * @param name the name * @param req HTTP request * @return the cookie value */ public static String getStateParam(String name, HttpServletRequest req) { return getCookieValue(req, name); } /** * Deletes a cookie. * @param name the name * @param req HTTP request * @param res HTTP response */ public static void removeStateParam(String name, HttpServletRequest req, HttpServletResponse res) { setRawCookie(name, "", req, res, false, 0); } /** * Sets a cookie. * @param name the name * @param value the value * @param req HTTP request * @param res HTTP response * @param httpOnly HTTP only flag * @param maxAge max age */ public static void setRawCookie(String name, String value, HttpServletRequest req, HttpServletResponse res, boolean httpOnly, int maxAge) { if (StringUtils.isBlank(name) || StringUtils.isBlank(value) || req == null || res == null) { return; } Cookie cookie = new Cookie(name, value); cookie.setHttpOnly(httpOnly); cookie.setMaxAge(maxAge < 0 ? Config.SESSION_TIMEOUT_SEC.intValue() : maxAge); cookie.setPath("/"); cookie.setSecure(req.isSecure()); res.addCookie(cookie); } /** * Reads a cookie. * @param name the name * @param req HTTP request * @return the cookie value */ public static String getCookieValue(HttpServletRequest req, String name) { if (StringUtils.isBlank(name) || req == null) { return null; } Cookie cookies[] = req.getCookies(); if (cookies == null || name == null || name.length() == 0) { return null; } //Otherwise, we have to do a linear scan for the cookie. for (Cookie cookie : cookies) { if (cookie.getName().equals(name)) { return cookie.getValue(); } } return null; } /** * Same as {@link java.lang.System#getProperty(java.lang.String)} * @param name the name of the property * @return the property value */ public static String getSystemProperty(String name) { return StringUtils.isBlank(name) ? "" : System.getProperty(name); } ///////////////////////////////////////////// // MISC UTILS ///////////////////////////////////////////// /** * Returns the adjusted size of an image (doesn't do any resizing) * @param h an image height * @param w an image width * @return the adjusted width and height if they are larger than {@link Config#MAX_IMG_SIZE_PX}. * @see Config#MAX_IMG_SIZE_PX */ public static int[] getMaxImgSize(int h, int w) { int[] size = { h, w }; int max = Config.MAX_IMG_SIZE_PX; if (w == h) { size[0] = Math.min(h, max); size[1] = Math.min(w, max); } else if (Math.max(h, w) > max) { int ratio = (100 * max) / Math.max(h, w); if (h > w) { size[0] = max; size[1] = (w * ratio) / 100; } else { size[0] = (h * ratio) / 100; size[1] = max; } } return size; } /** * Checks if a request comes from JavaScript. * @param request HTTP request * @return true if AJAX */ public static boolean isAjaxRequest(HttpServletRequest request) { return "XMLHttpRequest".equalsIgnoreCase(request.getHeader("X-Requested-With")) || "XMLHttpRequest".equalsIgnoreCase(request.getParameter("X-Requested-With")); } /** * Checks if a response is of type JSON. * @param contentType the value of "Content-Type" header * @return true if JSON */ public static boolean isJsonType(String contentType) { return StringUtils.startsWith(contentType, "application/json") || StringUtils.startsWith(contentType, "application/javascript") || StringUtils.startsWith(contentType, "text/javascript"); // F U facebook! } /** * Quick and dirty singular to plural conversion. * @param singul a word * @return a guess of its plural form */ public static String singularToPlural(String singul) { return StringUtils.isBlank(singul) ? singul : (singul.endsWith("s") ? singul + "es" : (singul.endsWith("y") ? StringUtils.removeEndIgnoreCase(singul, "y") + "ies" : singul + "s")); } /** * Checks if a class is primitive, String or a primitive wrapper. * * @param clazz a class * @return true if primitive or wrapper */ public static boolean isBasicType(Class<?> clazz) { return (clazz == null) ? false : (clazz.isPrimitive() || clazz.equals(String.class) || clazz.equals(Long.class) || clazz.equals(Integer.class) || clazz.equals(Boolean.class) || clazz.equals(Byte.class) || clazz.equals(Short.class) || clazz.equals(Float.class) || clazz.equals(Double.class) || clazz.equals(Character.class)); } /** * Returns the simple name of a class in lowercase (AKA the type). * * @param clazz a core class * @return just the name in lowercase or an empty string if clazz is null */ public static String type(Class<? extends ParaObject> clazz) { return (clazz == null) ? "" : clazz.getSimpleName().toLowerCase(); } ///////////////////////////////////////////// // ANNOTATIONS ///////////////////////////////////////////// /** * Returns a list of all declared fields in a class. Transient and serialVersionUID fields are skipped. * This method scans parent classes as well. * @param clazz a class to scan * @return a list of fields including those of the parent classes excluding the Object class. */ public static List<Field> getAllDeclaredFields(Class<? extends ParaObject> clazz) { LinkedList<Field> fields = new LinkedList<Field>(); if (clazz == null) { return fields; } Class<?> parent = clazz; do { for (Field field : parent.getDeclaredFields()) { if (!Modifier.isTransient(field.getModifiers()) && !field.getName().equals("serialVersionUID")) { fields.add(field); } } parent = parent.getSuperclass(); } while (!parent.equals(Object.class)); return fields; } ///////////////////////////////////////////// // MODIFIED SNOWFLAKE ///////////////////////////////////////////// /** * Distributed id generator. Relies on node/worker ids and datacenter ids to prevent collisions. * @return a long unique ID string of digits */ public static String getNewId() { // unique across JVMs as long as each has a different workerID // based on Twitter's Snowflake algorithm long timestamp = timestamp(); if (lastTimestamp == timestamp) { sequence = (sequence + 1) & sequenceMask; if (sequence == 0) { timestamp = tilNextMillis(lastTimestamp); } } else { sequence = 0; } if (timestamp < lastTimestamp) { throw new IllegalStateException( String.format("Clock moved backwards. " + "Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); } lastTimestamp = timestamp; return Long.toString(((timestamp - TIMER_OFFSET) << timestampLeftShift) | (dataCenterId << dataCenterIdShift) | (workerId << workerIdShift) | (sequence)); } private static long tilNextMillis(long lastTimestamp) { long timestamp = timestamp(); while (timestamp <= lastTimestamp) { timestamp = timestamp(); } return timestamp; } }