001/* 002 * Stallion Core: A Modern Web Framework 003 * 004 * Copyright (C) 2015 - 2016 Stallion Software LLC. 005 * 006 * This program is free software: you can redistribute it and/or modify it under the terms of the 007 * GNU General Public License as published by the Free Software Foundation, either version 2 of 008 * the License, or (at your option) any later version. This program is distributed in the hope that 009 * it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of 010 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public 011 * License for more details. You should have received a copy of the GNU General Public License 012 * along with this program. If not, see <http://www.gnu.org/licenses/gpl-2.0.html>. 013 * 014 * 015 * 016 */ 017 018package io.stallion.utils; 019 020import com.fasterxml.jackson.core.JsonProcessingException; 021import io.stallion.Context; 022import io.stallion.dataAccess.Model; 023import io.stallion.services.Log; 024import io.stallion.users.Role; 025import io.stallion.utils.json.JSON; 026import io.stallion.utils.json.RestrictedViews; 027import org.apache.commons.codec.binary.Base32; 028import org.apache.commons.codec.binary.Base64; 029import org.apache.commons.io.FilenameUtils; 030import org.apache.commons.lang3.StringUtils; 031import org.apache.commons.lang3.exception.ExceptionUtils; 032import org.apache.http.client.utils.URLEncodedUtils; 033 034import javax.mail.internet.AddressException; 035import javax.mail.internet.InternetAddress; 036import java.io.UnsupportedEncodingException; 037import java.net.URLEncoder; 038import java.nio.ByteBuffer; 039import java.security.SecureRandom; 040import java.text.Normalizer; 041import java.text.NumberFormat; 042import java.time.Instant; 043import java.time.ZoneId; 044import java.time.ZonedDateTime; 045import java.time.format.DateTimeFormatter; 046import java.util.Date; 047import java.util.Locale; 048import java.util.Map; 049import java.util.Random; 050import java.util.regex.Pattern; 051 052import static io.stallion.utils.Literals.UTF8; 053import static io.stallion.utils.Literals.map; 054import static io.stallion.utils.Literals.val; 055 056public class GeneralUtils { 057 058 public static final Object NULL = null; 059 060 public static final DateTimeFormatter DEFAULT_FORMAT = DateTimeFormatter.ofPattern("MMM d, YYYY h:mm a"); 061 public static final DateTimeFormatter SLUG_FORMAT = DateTimeFormatter.ofPattern("YYYY-MM-dd-HHmm-ssSS"); 062 public static final DateTimeFormatter ISO_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'"); 063 public static final DateTimeFormatter SQL_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); 064 065 public static final ZoneId UTC = ZoneId.of("UTC"); 066 private static final Pattern NONLATIN = Pattern.compile("[^\\w-]"); 067 private static final Pattern WHITESPACE = Pattern.compile("[\\s]"); 068 private static final Pattern MULTIHYPHENS = Pattern.compile("\\-\\-+"); 069 // Because probeContentType doesn't work on all platforms; 070 private static final Map<String, String> mimeTypes = map( 071 val("css", "text/css"), 072 val("js", "text/javascript"), 073 val("tag", "text/javascript"), 074 val("woff", "application/font-woff"), 075 val("otf", "application/octet-stream"), 076 val("eot", "application/octet-stream"), 077 val("ttf", "application/octet-stream"), 078 val("map", "application/json"), 079 val("json", "application/json") 080 ); 081 082 /** 083 * Converts the string into a string containing only hyphens, lower-case letters, and numbers, removing all 084 * other characters. 085 * 086 * @param input 087 * @return 088 */ 089 public static String slugify(String input) { 090 String nowhitespace = WHITESPACE.matcher(input).replaceAll("-"); 091 String normalized = Normalizer.normalize(nowhitespace, Normalizer.Form.NFD); 092 String slug = MULTIHYPHENS.matcher(NONLATIN.matcher(normalized).replaceAll("-")).replaceAll("-"); 093 return slug.toLowerCase(Locale.ENGLISH); 094 } 095 096 public static String formatCurrency(Double amt) { 097 NumberFormat formatter = NumberFormat.getCurrencyInstance(); 098 return formatter.format(amt); 099 } 100 101 public static String formatCurrency(Float amt) { 102 NumberFormat formatter = NumberFormat.getCurrencyInstance(); 103 return formatter.format(amt); 104 } 105 106 107 public static String urlEncode(String s) { 108 try { 109 return URLEncoder.encode(s, "utf-8"); 110 } catch (UnsupportedEncodingException e) { 111 throw new RuntimeException(s); 112 } 113 } 114 115 public static String guessMimeType(String path) { 116 return mimeTypes.getOrDefault(FilenameUtils.getExtension(path), null); 117 } 118 119 public static String md5Hash(String val) { 120 try { 121 java.security.MessageDigest md = java.security.MessageDigest.getInstance("MD5"); 122 byte[] array = md.digest(val.getBytes()); 123 StringBuffer sb = new StringBuffer(); 124 for (int i = 0; i < array.length; ++i) { 125 sb.append(Integer.toHexString((array[i] & 0xFF) | 0x100).substring(1,3)); 126 } 127 return sb.toString(); 128 } catch (java.security.NoSuchAlgorithmException e) { 129 throw new RuntimeException(e); 130 } 131 } 132 133 public static boolean isValidEmailAddress(String email) { 134 boolean result = true; 135 try { 136 InternetAddress emailAddr = new InternetAddress(email); 137 emailAddr.validate(); 138 } catch (AddressException ex) { 139 result = false; 140 } 141 return result; 142 } 143 144 145 // DEPRECATED methods 146 147 148 @Deprecated 149 public static ZonedDateTime utcNow() { 150 return ZonedDateTime.now(UTC); 151 } 152 153 @Deprecated 154 public static ZonedDateTime localNow() { 155 return ZonedDateTime.now(Context.getSettings().getTimeZoneId()); 156 } 157 158 /* Current milliseconds since the epoch */ 159 @Deprecated 160 public static long mils() { 161 return new Date().getTime(); 162 } 163 164 /* Epoch milliseconds to a ZonedDateTime */ 165 @Deprecated 166 public static ZonedDateTime milsToDateTime(long mils) { 167 return ZonedDateTime.ofInstant(Instant.ofEpochMilli(mils), UTC); 168 } 169 170 @Deprecated 171 public static String formatNow(String format) { 172 DateTimeFormatter formatter = DateTimeFormatter.ofPattern(format); 173 return utcNow().format(formatter); 174 } 175 176 177 178 /* We have to add this generic Object overload, because Method typing dispatching 179 * does not work correctly when called from the templates. So instead we have to 180 * include the type in the name of each function. Blech. 181 * */ 182 @Deprecated 183 public static String formatLocalDate(Object dt, String formatPattern) { 184 if (dt instanceof Long) { 185 return formatLocalDateFromLong((Long) dt, formatPattern); 186 } else if (dt instanceof ZonedDateTime) { 187 return formatLocalDateFromZonedDate((ZonedDateTime) dt, formatPattern); 188 } else if (dt instanceof Date) { 189 return formatLocalDateFromJDate((Date) dt, formatPattern); 190 } 191 return ""; 192 } 193 194 @Deprecated 195 public static String formatLocalDate(Object dt) { 196 return formatLocalDate(dt, null); 197 } 198 199 @Deprecated 200 public static String formatLocalDateFromJDate(Date date) { 201 return formatLocalDateFromJDate(date, null); 202 } 203 204 @Deprecated 205 public static String formatLocalDateFromJDate(Date date, String formatPattern) { 206 if (date == null) { 207 return ""; 208 } 209 return formatLocalDateFromZonedDate(ZonedDateTime.ofInstant(date.toInstant(), UTC), formatPattern); 210 } 211 212 @Deprecated 213 public static String formatLocalDateFromLong(long epochMillis) { 214 return formatLocalDateFromLong(epochMillis, null); 215 } 216 217 @Deprecated 218 public static String formatLocalDateFromLong(long epochMillis, String formatPattern) { 219 if (epochMillis == 0L) { 220 return ""; 221 } 222 return formatLocalDateFromZonedDate(ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), UTC), formatPattern); 223 } 224 225 @Deprecated 226 public static String formatLocalDateFromLong(Long epochMillis) { 227 return formatLocalDateFromLong(epochMillis, null); 228 } 229 230 @Deprecated 231 public static String formatLocalDateFromLong(Long epochMillis, String formatPattern) { 232 if (epochMillis == 0L || epochMillis == null) { 233 return ""; 234 } 235 return formatLocalDateFromZonedDate(ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), UTC), formatPattern); 236 } 237 238 @Deprecated 239 public static String formatLocalDateFromZonedDate(ZonedDateTime date) { 240 return formatLocalDateFromZonedDate(date, null); 241 } 242 243 244 /** 245 * Turn the given long id into a random base32 string token. This can be used for generating unique, secret strings 246 * for accessing data, such as a web page only viewable by a secret string. By using the long id, of the 247 * underlying object we guarantee uniqueness, by adding on random characters, we make the URL nigh 248 * impossible to guess. 249 * 250 * @param id - a long id that will be convered to base32 and used as the first part of the string 251 * @param length - the number of random base32 characters to add to the end of the string 252 * @return 253 */ 254 public static String tokenForId(Long id, int length) { 255 ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES); 256 buffer.putLong(id); 257 return StringUtils.stripStart(new Base32().encodeAsString(buffer.array()), "A").replace("=", "").toLowerCase() + "8" + randomTokenBase32(length); 258 } 259 260 /** 261 * Psuedo-random string of the given length, in base32 characters 262 * @param length 263 * @return 264 */ 265 public static String randomTokenBase32(int length) { 266 byte[] r = new byte[256]; //Means 2048 bit 267 new Random().nextBytes(r); 268 String s = new Base32().encodeAsString(r).substring(0, length).toLowerCase(); 269 return s; 270 271 } 272 273 /** 274 * Generates a random string using the SecureRandom module, of the given length, 275 * using URL safe base64 characters 276 * 277 * @param length 278 * @return 279 */ 280 public static String secureRandomToken(int length) { 281 SecureRandom random = new SecureRandom(); 282 byte bytes[] = new byte[length * 4]; 283 random.nextBytes(bytes); 284 String s = Base64.encodeBase64URLSafeString(bytes).substring(0, length); 285 return s; 286 } 287 288 /** 289 * Generates a random string using the psuedo-random module, of the given length, 290 * using URL safe base64 characters 291 * 292 * @param length 293 * @return 294 */ 295 public static String randomToken(int length) { 296 byte[] r = new byte[256]; //Means 2048 bit 297 new Random().nextBytes(r); 298 String s = Base64.encodeBase64URLSafeString(r).substring(0, length); 299 return s; 300 } 301 302 @Deprecated 303 public static String formatLocalDateFromZonedDate(ZonedDateTime date, String formatPattern) { 304 if (date == null) { 305 return ""; 306 } 307 308 ZonedDateTime localDt = date.withZoneSameInstant(Context.getSettings().getTimeZoneId()); 309 310 DateTimeFormatter formatter; 311 if (StringUtils.isEmpty(formatPattern)) { 312 formatter = DEFAULT_FORMAT; 313 } else if ("iso".equals(formatPattern.toLowerCase())) { 314 formatter = DateTimeFormatter.ISO_OFFSET_DATE_TIME; 315 } else { 316 formatter = DateTimeFormatter.ofPattern(formatPattern); 317 } 318 return localDt.format(formatter); 319 } 320 321 @Deprecated 322 public static String slugifyDate(Long epochMillis) { 323 return slugifyDate(ZonedDateTime.ofInstant(Instant.ofEpochMilli(epochMillis), UTC)); 324 } 325 326 @Deprecated 327 public static String slugifyDate(ZonedDateTime date) { 328 return date.format(SLUG_FORMAT); 329 } 330 331 public static Object htmlSafeJson(Object obj) { 332 333 String out = JSON.stringify(obj); 334 out = out.replace("<", "\\u003c"); 335 return out; 336 } 337 338 /** 339 * Gets the object in a JSON form that is safe for being outputted on a web page: 340 * <script> 341 * var myObj = {{ utils.htmlSafeJson(obj, "member") }} 342 * <script> 343 * @param obj 344 * @param restrictionLevel - Uses the JsonView annotation to determine which properties of the object 345 * should be outputed. Possible values are: unrestricted/public/member/owner/internal 346 * @return 347 */ 348 @Deprecated 349 public static Object htmlSafeJson(Object obj, String restrictionLevel) { 350 String out = ""; 351 try { 352 restrictionLevel = restrictionLevel == null ? "public" : restrictionLevel.toLowerCase(); 353 if ("public".equals(restrictionLevel)) { 354 out = JSON.stringify(obj, RestrictedViews.Public.class, true); 355 } else if ("unrestricted".equals(restrictionLevel)) { 356 out = JSON.stringify(obj, RestrictedViews.Unrestricted.class, false); 357 } else if ("member".equals(restrictionLevel)) { 358 out = JSON.stringify(obj, RestrictedViews.Member.class, true); 359 } else if ("owner".equals(restrictionLevel)) { 360 out = JSON.stringify(obj, RestrictedViews.Owner.class, true); 361 } else if ("internal".equals(restrictionLevel)) { 362 out = JSON.stringify(obj, RestrictedViews.Internal.class, true); 363 } else { 364 out = "Unknown restriction level: " + restrictionLevel; 365 } 366 } catch (JsonProcessingException ex) { 367 String objId = obj.toString(); 368 if (obj instanceof Model) { 369 objId = ((Model)obj).getId().toString(); 370 } 371 String msg = "Error JSON.stringifying object {0}" + obj.getClass().getSimpleName() + ":" + objId; 372 if (Context.getSettings().getDebug() || Context.getUser().isInRole(Role.ADMIN)) { 373 out = msg + "\n\nStacktrace-----\n\n" + ExceptionUtils.getStackTrace(ex); 374 } 375 Log.exception(ex, msg); 376 } 377 out = out.replace("<", "\\u003c"); 378 return out; 379 } 380 381 382} 383 384