Package com.erudika.para.utils

Source Code of com.erudika.para.utils.Utils

/*
* Copyright 2013-2014 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.annotations.Locked;
import com.erudika.para.annotations.Stored;
import com.erudika.para.core.ParaObject;
import com.erudika.para.core.Sysprop;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.ObjectReader;
import com.fasterxml.jackson.databind.ObjectWriter;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.github.rjeschke.txtmark.Configuration;
import com.github.rjeschke.txtmark.Processor;
import java.io.UnsupportedEncodingException;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
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.util.ArrayList;
import java.util.Base64;
import java.util.Calendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.ConstraintViolation;
import javax.validation.Validation;
import javax.validation.Validator;
import javax.validation.ValidatorFactory;
import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;
import org.apache.commons.beanutils.BeanUtils;
import org.apache.commons.beanutils.PropertyUtils;
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.geonames.FeatureClass;
import org.geonames.Style;
import org.geonames.Toponym;
import org.geonames.ToponymSearchCriteria;
import org.geonames.ToponymSearchResult;
import org.geonames.WebService;
import org.hibernate.validator.constraints.NotBlank;
import org.hibernate.validator.constraints.NotEmpty;
import org.jsoup.Jsoup;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.crypto.bcrypt.BCrypt;

/**
* The core utility class.
* @author Alex Bogdanovski [alex@erudika.com]
*/
@SuppressWarnings("unchecked")
public final class Utils {

  private static final Logger logger = LoggerFactory.getLogger(Utils.class);
  private static final ExecutorService exec = Executors.newSingleThreadExecutor();
  private static final ObjectMapper jsonMapper = new ObjectMapper();
  private static final ObjectReader jsonReader = jsonMapper.reader();
  private static final ObjectWriter jsonWriter = jsonMapper.writer();
  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();
    jsonMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
    jsonMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
    jsonMapper.enable(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT);
    jsonMapper.enable(SerializationFeature.INDENT_OUTPUT);
  }

  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;
  }

  /**
   * A Jackson {@code ObjectMapper}.
   * @return JSON object mapper
   */
  public static ObjectMapper getJsonMapper() {
    return jsonMapper;
  }

  /**
   * A Jackson JSON reader.
   * @param type the type to read
   * @return JSON object reader
   */
  public static ObjectReader getJsonReader(Class<?> type) {
    return jsonReader.withType(type);
  }

  /**
   * A Jackson JSON writer. Pretty print is on.
   * @return JSON object writer
   */
  public static ObjectWriter getJsonWriter() {
    return jsonWriter.withFeatures(SerializationFeature.INDENT_OUTPUT);
  }

  /**
   * A Jackson JSON writer. Pretty print is off.
   * @return JSON object writer with indentation disabled
   */
  public static ObjectWriter getJsonWriterNoIdent() {
    return jsonWriter;
  }

  /////////////////////////////////////////////
  //           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
   * @return a random string
   */
  public static String generateSecurityToken(int length) {
    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 base64enc(bytes);
  }

  /**
   * 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);
  }

  /**
   * 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 StringUtils.isBlank(str) ? "" :
      str.replaceAll("[\\p{S}\\p{P}\\p{C}]", "").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 {
      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.getEncoder().encode(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.getDecoder().decode(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_DATETIME_FORMAT.getPattern();
    }
    if (timestamp == null) {
      timestamp = timestamp();
    }
    if (loc == null) {
      loc = Locale.US;
    }
    return DateFormatUtils.format(timestamp, format, loc);
  }

  /**
   * Formats a date in a specific format.
   * @param format the date format
   * @param loc the locale instance
   * @return a formatted date
   */
  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 time
   * @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);
  }

  /**
   * 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 checker
   * @param url a URL
   * @return true if the URL is valid
   */
  public static boolean isValidURL(String url) {
    return !StringUtils.isBlank(getHostFromURL(url));
  }

  /**
   * 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: &gt;scheme&lt;:&gt;authority&lt;
   * @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;
  }

  /**
   * Returns a list of places for a query. Data from geonames.org
   * @param q the query
   * @return a list of places
   */
  public static List<Toponym> readLocationForKeyword(String q) {
    return readLocationForKeyword(q, Style.FULL);
  }

  /**
   * Returns a list of places for a query. Data from geonames.org
   * @param q the query
   * @param style style
   * @return a list of places
   */
  public static List<Toponym> readLocationForKeyword(String q, Style style) {
    List<Toponym> list = new ArrayList<Toponym>();
    ToponymSearchResult locationSearchResult;
    ToponymSearchCriteria searchLocation = new ToponymSearchCriteria();
    searchLocation.setMaxRows(7);
    searchLocation.setFeatureClass(FeatureClass.P);
    searchLocation.setStyle(style);
    searchLocation.setQ(q);
    try {
      WebService.setUserName("erudika");
      locationSearchResult = WebService.search(searchLocation);
      if (locationSearchResult != null) {
        list.addAll(locationSearchResult.getToponyms());
      }
    } catch (Exception ex) {
      logger.error(null, ex);
    }
    return list;
  }

  /**
   * 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"));
  }

  /**
   * Executes a {@link java.util.concurrent.Callable} asynchronously
   * @param callable a task
   * @return a future
   */
  public static Future<?> asyncExecute(Callable<?> callable) {
    try {
      return exec.submit(callable);
    } catch (Exception ex) {
      logger.warn(null, ex);
      return null;
    }
  }

  /**
   * 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"));
  }


  /////////////////////////////////////////////
  //       OBJECT MAPPING & CLASS UTILS
  /////////////////////////////////////////////

  /**
   * Populates an object with an array of query parameters (dangerous!).
   * This method might be deprecated in the future.
   * @param <P> the object type
   * @param transObject an object
   * @param paramMap a query parameters map
   */
  public static <P extends ParaObject> void populate(P transObject, Map<String, String[]> paramMap) {
    if (transObject == null || paramMap == null || paramMap.isEmpty()) {
      return;
    }
    Class<Locked> locked = (paramMap.containsKey(Config._ID)) ? Locked.class : null;
    Map<String, Object> fields = getAnnotatedFields(transObject, locked);
    Map<String, Object> data = new HashMap<String, Object>();
    // populate an object with converted param values from param map.
    try {
      for (Map.Entry<String, String[]> ks : paramMap.entrySet()) {
        String param = ks.getKey();
        String[] values = ks.getValue();
        String value = (values.length > 1) ? getJsonWriter().writeValueAsString(values) : values[0];
        if (fields.containsKey(param)) {
          data.put(param, value);
        }
      }
      setAnnotatedFields(transObject, data, locked);
    } catch (Exception ex) {
      logger.error(null, ex);
    }
  }

  /**
   * Checks if the type of an object matches its real Class name.
   * @param so an object
   * @return true if the types match
   */
  public static boolean typesMatch(ParaObject so) {
    return (so == null) ? false : so.getClass().equals(toClass(so.getType()));
  }

  /**
   * Returns a map of annotated fields of a domain object. Only annotated fields are returned.
   * This method forms the basis of an Object/Grid Mapper. It converts an object
   * to a map of key/value pairs. That map can later be persisted to a data store.
   * <br>
   * Field values that are objects (i.e. not primitive types or wrappers) are converted to JSON.
   * Null is considered a primitive type. Transient fields and serialVersionUID are skipped.
   * @param <P> the object type
   * @param bean the object to convert to a map
   * @return a map of fields and their values
   */
  public static <P extends ParaObject> Map<String, Object> getAnnotatedFields(P bean) {
    return getAnnotatedFields(bean, null);
  }

  /**
   * Returns a map of annotated fields of a domain object. Only annotated fields are returned.
   * This method forms the basis of an Object/Grid Mapper. It converts an object
   * to a map of key/value pairs. That map can later be persisted to a data store.
   * <br>
   * Field values that are objects (i.e. not primitive types or wrappers) are converted to JSON.
   * Null is considered a primitive type. Transient fields and serialVersionUID are skipped.
   * @param <P> the object type
   * @param bean the object to convert to a map
   * @param filter a filter annotation. fields that have it will be skipped
   * @return a map of fields and their values
   */
  public static <P extends ParaObject> Map<String, Object> getAnnotatedFields(P bean,
      Class<? extends Annotation> filter) {
    HashMap<String, Object> map = new HashMap<String, Object>();
    if (bean == null) {
      return map;
    }
    try {
      List<Field> fields = getAllDeclaredFields(bean.getClass());
      // filter transient fields and those without annotations
      for (Field field : fields) {
        boolean dontSkip = ((filter == null) ? true : !field.isAnnotationPresent(filter));
        if (field.isAnnotationPresent(Stored.class) && dontSkip) {
          Object prop = PropertyUtils.getProperty(bean, field.getName());
          if (prop == null || isBasicType(field.getType())) {
            map.put(field.getName(), prop);
          } else {
            map.put(field.getName(), getJsonWriterNoIdent().writeValueAsString(prop));
          }
        }
      }
    } catch (Exception ex) {
      logger.error(null, ex);
    }

    return map;
  }

  /**
   * Converts a map of fields/values to a domain object. Only annotated fields are populated.
   * This method forms the basis of an Object/Grid Mapper.
   * <br>
   * Map values that are JSON objects are converted to their corresponding Java types.
   * Nulls and primitive types are preserved.
   * @param <P> the object type
   * @param data the map of fields/values
   * @return the populated object
   */
  public static <P extends ParaObject> P setAnnotatedFields(Map<String, Object> data) {
    return setAnnotatedFields(null, data, null);
  }

  /**
   * Converts a map of fields/values to a domain object. Only annotated fields are populated.
   * This method forms the basis of an Object/Grid Mapper.
   * <br>
   * Map values that are JSON objects are converted to their corresponding Java types.
   * Nulls and primitive types are preserved.
   * @param <P> the object type
   * @param bean the object to populate with data
   * @param data the map of fields/values
   * @param filter a filter annotation. fields that have it will be skipped
   * @return the populated object
   */
  public static <P extends ParaObject> P setAnnotatedFields(P bean, Map<String, Object> data,
      Class<? extends Annotation> filter) {
    if (data == null || data.isEmpty()) {
      return null;
    }
    try {
      if (bean == null) {
        // try to find a declared class in the core package
        bean = (P) toClass((String) data.get(Config._TYPE)).getConstructor().newInstance();
      }
      List<Field> fields = getAllDeclaredFields(bean.getClass());
      Map<String, Object> props = new HashMap<String, Object>(data);
      for (Field field : fields) {
        boolean dontSkip = ((filter == null) ? true : !field.isAnnotationPresent(filter));
        if (field.isAnnotationPresent(Stored.class) && dontSkip) {
          String name = field.getName();
          Object value = data.get(name);
          // try to read a default value from the bean if any
          if (value == null && PropertyUtils.isReadable(bean, name)) {
            value = PropertyUtils.getProperty(bean, name);
          }
          if (value == null || isBasicType(field.getType())) {
            BeanUtils.setProperty(bean, name, value);
          } else {
            Object val = getJsonReader(field.getType()).readValue(value.toString());
            BeanUtils.setProperty(bean, name, val);
          }
          props.remove(name);
        }
      }
    } catch (Exception ex) {
      logger.error(null, ex);
      bean = null;
    }
    return bean;
  }

  /**
   * Constructs a new instance of a core object.
   * @param <P> the object type
   * @param type the simple name of a class
   * @return a new instance of a core class. Defaults to {@link com.erudika.para.core.Sysprop}.
   * @see #toClass(java.lang.String)
   */
  public static <P extends ParaObject> P toObject(String type) {
    try {
      return (P) toClass(type).getConstructor().newInstance();
    } catch (Exception ex) {
      logger.error(null, ex);
      return null;
    }
  }

  /**
   * Converts a class name to a real Class object.
   * @param type the simple name of a class
   * @return the Class object or {@link com.erudika.para.core.Sysprop} if the class was not found.
   * @see java.lang.Class#forName(java.lang.String)
   */
  public static Class<? extends ParaObject> toClass(String type) {
    return toClass(type, null, Sysprop.class);
  }

  /**
   * Converts a class name to a real {@link com.erudika.para.core.ParaObject} subclass.
   * Defaults to {@link com.erudika.para.core.Sysprop} if the class was not found in the core package path.
   * @param type the simple name of a class
   * @param scanPackagePath the package path of the class
   * @param defaultClass returns this type if the requested class was not found on the classpath.
   * @return the Class object. Returns null if defaultClass is null.
   * @see java.lang.Class#forName(java.lang.String)
   * @see com.erudika.para.core.Sysprop
   */
  public static Class<? extends ParaObject> toClass(String type, String scanPackagePath,
      Class<? extends ParaObject> defaultClass) {
    String packagename = Config.CORE_PACKAGE_NAME;
    String corepackage = StringUtils.isBlank(scanPackagePath) ? packagename : scanPackagePath;
    Class<? extends ParaObject> returnClass = defaultClass;
    if (StringUtils.isBlank(type)) {
      return returnClass;
    }
    try {
      returnClass = (Class<? extends ParaObject>) Class.forName(ParaObject.class.getPackage().getName().
          concat(".").concat(StringUtils.capitalize(type)));
    } catch (ClassNotFoundException ex) {
      try {
        returnClass = (Class<? extends ParaObject>) Class.forName(corepackage.concat(".").
            concat(StringUtils.capitalize(type)));
      } catch (ClassNotFoundException ex1) {
        // unknown type - fall back to Sysprop
        returnClass = defaultClass;
      }
    }
    return returnClass;
  }

  /**
   * Converts a JSON string to a domain object.
   * If we can't match the JSON to a core object, we fall back to {@link com.erudika.para.core.Sysprop}.
   * @param <P> type of object to convert
   * @param json the JSON string
   * @return a core domain object or null if the string was blank
   */
  public static <P extends ParaObject> P fromJSON(String json) {
    if (StringUtils.isBlank(json)) {
      return null;
    }
    try {
      Map<String, Object> map = getJsonReader(Map.class).readValue(json);
      return setAnnotatedFields(map);
    } catch (Exception e) {
      logger.error(null, e);
    }
    return null;
  }

  /**
   * Converts a domain object to JSON.
   * @param <P> type of object to convert
   * @param obj a domain object
   * @return the JSON representation of that object
   */
  public static <P extends ParaObject> String toJSON(P obj) {
    if (obj == null) {
      return "{}";
    }
    try {
      return getJsonWriter().writeValueAsString(obj);
    } catch (Exception e) {
      logger.error(null, e);
    }
    return "{}";
  }

  /**
   * 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 & VALIDATION
  /////////////////////////////////////////////

  /**
   * Validates objects using Hibernate Validator.
   * @param obj an object to be validated
   * @return true if the object is valid (all fields are populated properly)
   */
  public static boolean isValidObject(ParaObject obj) {
    return validateObject(obj).length == 0;
  }

  /**
   * Validates objects using Hibernate Validator.
   * @param content an object to be validated
   * @return a list of error messages or empty if object is valid
   */
  public static String[] validateObject(ParaObject content) {
    if (content == null) {
      return new String[]{ "Object cannot be null." };
    }
    ArrayList<String> list = new ArrayList<String>();
    try {
      ValidatorFactory factory = Validation.buildDefaultValidatorFactory();
      Validator validator = factory.getValidator();
      for (ConstraintViolation<ParaObject> constraintViolation : validator.validate(content)) {
        String prop = "'".concat(constraintViolation.getPropertyPath().toString()).concat("'");
        list.add(prop.concat(" ").concat(constraintViolation.getMessage()));
      }
    } catch (Exception e) {
      logger.error(null, e);
    }
    return list.toArray(new String[]{ });
  }

  /**
   * A JSON object used for JavaScript validation. It maps Hibernate Validator annotations to JavaScript.
   * This method WILL change in the future.
   * @param type the type of object that will be validated
   * @param fields list of fields to map
   * @param lang language map
   * @return the JSON validation object
   */
  public static String getJSONValidationObject(String type, List<String> fields, Map<String, String> lang) {
    String root = "{}";
    if (fields == null) {
      fields = new ArrayList<String>();
    }
    if (!StringUtils.isBlank(type)) {
      Class<? extends ParaObject> c = toClass(type);
      ArrayList<String> rules = new ArrayList<String>();
      ArrayList<String> messages = new ArrayList<String>();
      for (Entry<String, List<Annotation>> entry : getAnnotationsMap(c, new HashSet<String>(fields)).entrySet()) {
        ArrayList<String> rs = new ArrayList<String>();
        ArrayList<String> ms = new ArrayList<String>();
        for (Annotation ano : entry.getValue()) {
          String[] rmarr = annotationToValidation(ano, lang);
          if (rmarr.length == 2) {
            String one = rmarr[0];
            String two = rmarr[1];
            if (!StringUtils.isBlank(one)) {
              rs.add(one);
            }
            if (!StringUtils.isBlank(two)) {
              ms.add(two);
            }
          }
        }
        if (!rs.isEmpty()) {
          rules.add("'" + entry.getKey() + "':{" + arrayJoin(rs, ", ") + "}");
        }
        if (!ms.isEmpty()) {
          messages.add("'" + entry.getKey() + "':{" + arrayJoin(ms, ", ") + "}");
        }
      }
      root = "{'rules':{" + arrayJoin(rules, ", ") + "}, 'messages':{" + arrayJoin(messages, ", ") + "}}";
    }
    return root;
  }

  /**
   * Returns a map of annotations present in for all the declared fields in an object.
   * @param clazz a class to scan
   * @param fields only scan these fields (leave null to scan everything)
   * @return a map of field names to the list of annotations they have
   */
  static Map<String, List<Annotation>> getAnnotationsMap(Class<? extends ParaObject> clazz, Set<String> fields) {
    HashMap<String, List<Annotation>> map = new HashMap<String, List<Annotation>>();
    try {
      List<Field> fieldlist = getAllDeclaredFields(clazz);

      if (fields != null && !fields.isEmpty()) {
        for (Iterator<Field> it = fieldlist.iterator(); it.hasNext();) {
          Field field = it.next();
          if (!fields.contains(field.getName())) {
            it.remove();
          }
        }
      }

      for (Field field : fieldlist) {
        Annotation[] annos = field.getAnnotations();
        if (annos.length > 1) {
          ArrayList<Annotation> list = new ArrayList<Annotation>();
          for (Annotation annotation : annos) {
            if (!annotation.annotationType().equals(Stored.class)) {
              list.add(annotation);
            }
          }
          map.put(field.getName(), list);
        }
      }
    } catch (Exception ex) {
      logger.error(null, ex);
    }
    return map;
  }

  /**
   * 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.
   */
  static List<Field> getAllDeclaredFields(Class<? extends ParaObject> clazz) {
    ArrayList<Field> fields = new ArrayList<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;
  }

  /**
   * A JSON object used for JavaScript validation. It maps Hibernate Validator annotations to JavaScript.
   * This method WILL change in the future.
   * @param ano annotation
   * @param lang language map
   * @return an array with two items: a rule and a message
   */
  static String[] annotationToValidation(Annotation ano,  Map<String, String> lang) {
    if (ano == null) {
      return new String[0];
    }
    if (lang == null) {
      lang = new HashMap<String, String>();
    }
    Class<? extends Annotation> atype = ano.annotationType();
    String rule = "";
    String msg = "";

    if (atype.equals(NotBlank.class) || atype.equals(NotEmpty.class) ||
        atype.equals(NotNull.class)) {
      rule = "'required': true";
      msg = "'required': '" + lang.get("requiredfield") + "'";
    } else if (atype.equals(Size.class)) {
      try {
        Integer min = (Integer) atype.getMethod("min").invoke(ano);
        Integer max = (Integer) atype.getMethod("max").invoke(ano);
        rule = "'minlength': " + min + ", 'maxlength': " + max;
        msg = "'minlength': '" + formatMessage(lang.get("minlength"), min) + "', " +
          "'maxlength': '" + formatMessage(lang.get("maxlength"), max) + "'";
      } catch (Exception ex) {
        logger.error(null, ex);
      }

    } else if (atype.equals(Email.class)) {
      rule = "'email': true";
      msg = "'email': '" + lang.get("bademail") + "'";
    }
    return new String[]{rule, msg};
  }

  /////////////////////////////////////////////
  //          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 synchronized 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;
  }

}
TOP

Related Classes of com.erudika.para.utils.Utils

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.