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     * &lt;script&gt;
341     *     var myObj = {{ utils.htmlSafeJson(obj, "member") }}
342     * &lt;script&gt;
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