Please wait. This can take some minutes ...
Many resources are needed to download a project. Please understand that we have to compensate our server costs. Thank you in advance.
Project price only 1 $
You can buy this project and download/modify it how often you want.
net.yadaframework.components.YadaUtil Maven / Gradle / Ivy
package net.yadaframework.components;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.math.BigInteger;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.text.DateFormat;
import java.text.Normalizer;
import java.text.NumberFormat;
import java.text.ParseException;
import java.text.ParsePosition;
import java.text.SimpleDateFormat;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collection;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
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.Optional;
import java.util.Set;
import java.util.SortedSet;
import java.util.Stack;
import java.util.TimeZone;
import java.util.TreeSet;
import java.util.concurrent.TimeUnit;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
import javax.annotation.Nullable;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.FileImageInputStream;
import javax.imageio.stream.ImageInputStream;
import org.apache.commons.exec.CommandLine;
import org.apache.commons.exec.DefaultExecutor;
import org.apache.commons.exec.ExecuteWatchdog;
import org.apache.commons.exec.PumpStreamHandler;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.SystemUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.MessageSource;
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.context.i18n.LocaleContextHolder;
import org.springframework.core.type.filter.RegexPatternTypeFilter;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
import com.drew.imaging.ImageMetadataReader;
import com.drew.metadata.Metadata;
import com.drew.metadata.exif.ExifIFD0Directory;
import com.drew.metadata.exif.ExifSubIFDDirectory;
import com.drew.metadata.gif.GifHeaderDirectory;
import com.drew.metadata.jpeg.JpegDirectory;
import jakarta.persistence.Entity;
import jakarta.validation.constraints.NotNull;
import net.yadaframework.core.CloneableDeep;
import net.yadaframework.core.CloneableFiltered;
import net.yadaframework.core.YadaConfiguration;
import net.yadaframework.exceptions.YadaInternalException;
import net.yadaframework.exceptions.YadaInvalidUsageException;
import net.yadaframework.exceptions.YadaInvalidValueException;
import net.yadaframework.exceptions.YadaSystemException;
import net.yadaframework.persistence.entity.YadaAttachedFile;
import net.yadaframework.raw.YadaIntDimension;
import sogei.utility.UCheckDigit;
import sogei.utility.UCheckNum;
@Component
public class YadaUtil {
private final static Logger log = LoggerFactory.getLogger(YadaUtil.class);
@Autowired private YadaConfiguration config;
@Autowired private AutowireCapableBeanFactory autowireCapableBeanFactory; // For autowiring entities
static private YadaFileManager yadaFileManager;
static public ApplicationContext applicationContext; // To access the ApplicationContext from anywhere
static public MessageSource messageSource; // To access the MessageSource from anywhere, injected by YadaAppConfig
public final static long MILLIS_IN_MINUTE = 60*1000;
public final static long MILLIS_IN_HOUR = 60*MILLIS_IN_MINUTE;
public final static long MILLIS_IN_DAY = 24*MILLIS_IN_HOUR;
private SecureRandom secureRandom = new SecureRandom();
private final static char CHAR_AT = '@';
private final static char CHAR_DOT = '.';
private final static char CHAR_SPACE = ' ';
private static Locale defaultLocale = null;
private List computedTimezoneOffsets = null;
private List computedTimezones = null;
/**
* Instance to be used when autowiring is not available
*/
public final static YadaUtil INSTANCE = new YadaUtil();
// @PostConstruct
@EventListener(ContextRefreshedEvent.class) // Called after the context has been initialized
public void init() {
defaultLocale = config.getDefaultLocale();
yadaFileManager = getBean(YadaFileManager.class);
}
/**
* Returns a list of files from a folder where the name contains the given string, sorted alphabetically
* @param folderPath the folder where to looks for files, excluding subfolders.
* @param contains a string that the name must contain, can be empty or null to accept any
* @return a list of files, can be empty
* @throws IOException
*/
public List getFilesInFolder(Path folderPath, String contains) throws IOException {
try (Stream paths = Files.list(folderPath)) {
return paths.filter(Files::isRegularFile)
.filter(path -> contains == null || path.getFileName().toString().contains(contains))
.sorted(Comparator.comparing(Path::getFileName))
.collect(Collectors.toList());
}
}
/**
* Returns a random string (currently an hex random number)
* @param minlen minimum length of the string. The maximum length is random.
* @return
*/
public String getRandomText(int minlen) {
int random = getRandom(0, Integer.MAX_VALUE);
return String.format("%0"+minlen+"X", random);
}
/**
* Joins a number of strings, adding a separator only when the strings are not empty.
* In other words, null or empty strings are skipped without adding a separator.
* @param separator
* @param toJoin
* @return
*/
public String joinIfNotEmpty(@NotNull String separator, String...toJoin) {
StringBuilder result = new StringBuilder();
for (String part : toJoin) {
if (StringUtils.isNotEmpty(part)) {
if (result.length()>0) {
result.append(separator);
}
result.append(part);
}
}
return result.toString();
}
/**
* Given a date in the past, returns a string like "12 minutes ago", "2 hours ago", "today at 12:51", "yesterday at 5:32"...
* For dates before yesterday, the full RFC_1123 format is used, as 'Tue, 3 Jun 2008 11:05:30 GMT'.
* No "x days ago" format is currently provided.
* @param timestamp
* @param locale
* @param maxHours the max value of x for using the "x hours ago" format after which the "today at hh:mm" format is used
* The default is 3 when null. There is no maximum value, in order to have a "76 hours ago" result if needed.
* @return
*/
public String getTimestampAsRelative(ZonedDateTime timestamp, Locale locale, Integer maxHours) {
maxHours = maxHours==null?3:maxHours;
final long MILLIS_PER_SECOND = 1000;
final long MILLIS_PER_MINUTE = MILLIS_PER_SECOND*60;
final long MILLIS_PER_HOUR = MILLIS_PER_MINUTE*60;
// final long MILLIS_PER_DAY = MILLIS_PER_HOUR*24;
long elapsedMillis = System.currentTimeMillis()-timestamp.toInstant().toEpochMilli();
//
// Small intervals up to maxHours
if (elapsedMillis >= 0 && elapsedMillis= 0 && elapsedMillis= 0 && elapsedMillis= 0 && elapsedMillis<(maxHours+1)*MILLIS_PER_HOUR) {
Long value = elapsedMillis / MILLIS_PER_HOUR;
return messageSource.getMessage("yada.timestamp.hoursago", new Object[] {value}, locale); // "3 hours ago"
}
//
// Medium intervals from maxHours up to yesterday
ZonedDateTime zonedNow = ZonedDateTime.now(timestamp.getZone());
long elapsedDays = daysBetween(timestamp, zonedNow);
if (elapsedDays>=0 && elapsedDays<2) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("H:m");
String hm = timestamp.format(formatter);
if (elapsedDays==0) {
// Today
return messageSource.getMessage("yada.timestamp.todayAt", new Object[] {hm}, locale); // "today at 12:43"
} else if (elapsedDays==1) {
// Yesterday
return messageSource.getMessage("yada.timestamp.yesterdayAt", new Object[] {hm}, locale); // "yesterday at 12:43"
}
}
//
// Long intervals: just show the full date and time
return timestamp.format(DateTimeFormatter.RFC_1123_DATE_TIME);
}
/**
* Parse a string as a double, using the correct decimal separator (if any).
* @param value a number that may have a decimal part
* @param locale
* @return a double
* @throws ParseException if the string is not a valid double in the locale specified
*/
public double stringToDouble(String value, Locale locale) throws ParseException {
// From https://stackoverflow.com/a/16879667/587641
NumberFormat numberFormat = NumberFormat.getNumberInstance(locale);
ParsePosition parsePosition = new ParsePosition(0);
Number number = numberFormat.parse(value, parsePosition);
if (parsePosition.getIndex() != value.length()){
throw new ParseException("Invalid double input: '" + value + "'", parsePosition.getIndex());
}
return number.doubleValue();
}
/**
* Add an element to the list only if the element is not null
* @param
* @param list
* @param element
*/
public void addIfNotNull(List list, T element) {
if (element!=null) {
list.add(element);
}
}
/**
* Add an element to the list only if not there already
* @param
* @param list
* @param element
*/
public void addIfMissing(List list, T element) {
if (!list.contains(element)) {
list.add(element);
}
}
/**
* Create a new TreeSet that sorts values according to the order specified in the parameter.
* Values that are missing from sortOrder are sorted alphabetically
* @param sortOrder
* @return an empty sorted set that can receive a subset of the values in the sortOrder and keep them sorted the same way
*/
public Set getEmptySortedSet(List sortOrder) {
Map order = new HashMap(); // From value to position
for (int j = 0; j < sortOrder.size(); j++) {
order.put(sortOrder.get(j), j);
}
Set result = new TreeSet<>(new Comparator() {
@Override
public int compare(String left, String right) {
try {
return order.get(left).compareTo(order.get(right));
} catch (Exception e) {
// In case of error, fallback to alphabetical
log.error("Can't compare {} with {} (ignored)", left, right);
if (left!=null) {
return left.compareTo(right);
}
return right!=null?1:0;
}
}
});
return result;
}
/**
* Returns a list of days between two dates included. Days are at midnight in the system timezone.
* @param fromInclusive start day, the time is ignored
* @param toInclusive end day, the time is ignored
*/
public List getDays(Date fromInclusive, Date toInclusive) {
LocalDate start = fromInclusive.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
LocalDate end = toInclusive.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
return getDays(start, end);
}
/**
* Returns a list of days between two dates included.
* @param fromInclusive start day, the time is ignored
* @param toInclusive end day, the time is ignored
*/
public List getDays(LocalDate fromInclusive, LocalDate toInclusive) {
List days = new ArrayList<>();
LocalDate currentDate = fromInclusive;
while (!currentDate.isAfter(toInclusive)) {
days.add(currentDate);
currentDate = currentDate.plusDays(1);
}
return days;
}
/**
* Given a ISO date, a ISO time and a timezone, return the Date.
* @param isoDateString like '2011-12-03'
* @param isoTimeString like '10:15' or '10:15:30' (optional, can be null or empty)
* @param timezone the timezone where the date/time strings belong (optional, can be null)
* @return a Date representing the datetime in the timezone, or null when invalid
*/
public Date getDateFromDateTimeIsoString(String isoDateString, String isoTimeString, TimeZone timezone) {
if (isoDateString==null) {
return null;
}
isoTimeString = StringUtils.trimToNull(isoTimeString);
String isoDateTimeString = isoDateString + (isoTimeString!=null? ("T" + isoTimeString) : "T00:00");
try {
LocalDateTime chosenDateTime = LocalDateTime.parse(isoDateTimeString);
if (timezone == null) {
timezone = TimeZone.getDefault();
}
ZonedDateTime chosenDateTimeZoned = chosenDateTime.atZone(timezone.toZoneId());
return Date.from(chosenDateTimeZoned.toInstant());
} catch (Exception e) {
log.error("Invalid ISO date/time (returning null)", e);
return null;
}
}
/**
* Returns a string for date and time in the specified timezone and locale
* @param date the date to format
* @param timezone the timezone in which the date is to be considered
* @param locale the locale to use for formatting
* @return The RFC-1123 formatted date, such as 'Tue, 3 Jun 2008 11:05:30 GMT'.
*/
public String getRfcDateTimeStringForTimezone(Date date, TimeZone timezone, Locale locale) {
DateTimeFormatter formatter = DateTimeFormatter.RFC_1123_DATE_TIME.withLocale(locale);
ZoneId zoneId = timezone.toZoneId();
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(date.toInstant(), zoneId);
return zonedDateTime.format(formatter);
}
/**
* Convert a date in the timezone to a ISO string, like '2011-12-03'
* @param date
* @param timezone
* @return
*/
public String getIsoDateStringForTimezone(Date date, TimeZone timezone) {
return formatDateTimeForTimezone(date, timezone, DateTimeFormatter.ISO_LOCAL_DATE);
}
/**
* Convert a time in the timezone to a ISO string, like '10:15' or '10:15:30'
* @param date
* @param timezone
* @return
*/
public String getIsoTimeStringForTimezone(Date time, TimeZone timezone) {
return formatDateTimeForTimezone(time, timezone, DateTimeFormatter.ISO_LOCAL_TIME);
}
/**
* Convert a datetime in the timezone to a ISO string, like '2011-12-03T10:15:30'
* @param date
* @param timezone
* @return
*/
public String getIsoDateTimeStringForTimezone(Date dateTime, TimeZone timezone) {
return formatDateTimeForTimezone(dateTime, timezone, DateTimeFormatter.ISO_LOCAL_DATE_TIME);
}
private String formatDateTimeForTimezone(Date datetime, TimeZone timezone, DateTimeFormatter formatter) {
if (datetime==null) {
return "";
}
ZoneId zoneId = timezone.toZoneId();
ZonedDateTime zonedDateTime = ZonedDateTime.ofInstant(datetime.toInstant(), zoneId);
return zonedDateTime.format(formatter);
}
/**
* Create a single empty "json" object for use in other methods.
* Json objects are actually maps that get converted by Spring on return.
* @return
*/
public Map makeJsonObject() {
return new HashMap();
}
/**
* Given a json stored as a map, returns the json at the specified key.
* Non need for this method if the objectPath is a simple key: just use the Map get(key) method.
* @param jsonSource
* @param objectPath the name of a (nested) json property holding an object
* @return the object or null if it does not exist
* @see #makeJsonObject(parentObject, path)
*/
@SuppressWarnings("unchecked")
public Map getJsonObject(Map jsonSource, String objectPath) {
return (Map) getJsonPath(jsonSource, objectPath);
}
/**
* Given a json stored as a map, returns the json at the specified list index
* @param jsonSource
* @param listPath the path of the json property holing the list
* @param listIndex the list index
* @return
*/
@SuppressWarnings("unchecked")
public Map getJsonObject(Map jsonSource, String listPath, int listIndex) {
return (Map) getJsonArray(jsonSource, listPath).get(listIndex);
}
/**
* Given a json stored as a map, returns the json array at the specified key
* Non need for this method if the objectPath is a simple key: just use the Map get(key) method.
* @param jsonSource
* @param objectPath
* @return
*/
@SuppressWarnings("unchecked")
public List getJsonArray(Map jsonSource, String objectPath) {
return (List) getJsonPath(jsonSource, objectPath);
}
/**
* Given a json stored as a map, returns the String value at the specified key, with optional nesting.
* Non need for this method if the objectPath is a simple key: just use the Map get(key) method.
* @param jsonSource
* @param objectPath the path of the attribute, using dot notation and arrays. E.g. "order.amount[2].currency"
* @return
*/
public String getJsonAttribute(Map jsonSource, String objectPath) {
return (String) getJsonPath(jsonSource, objectPath);
}
/**
* Creates an empty json object at the given path
* @param parentObject the json object containing the new object
* @param path where the object should be created, using dot notation and arrays. E.g. "order.amount[2].currency".
* Any missing array cells are also created with an empty object as needed.
* @return
*/
public Map makeJsonObject(Map parentObject, String path) {
return (Map) setJsonAttributeRecurse(parentObject, path, null);
}
/**
* Sets a json property at the given path. Non need for this method if the path is a simple key: just use the Map put(key, value) method.
* @param jsonObject the json object that should contain the property
* @param path path of the property using dot notation and arrays. E.g. "order.amount[2].currency".
* Any missing array cells are also created with an empty object as needed.
* @param value the value to store, can also be a "json object"
*/
public void setJsonAttribute(Map jsonObject, String path, Object value) {
boolean isString = value instanceof String;
boolean isJsonObject = value instanceof Map;
if (!isString && !isJsonObject) {
value = String.valueOf(value);
// throw new YadaInvalidValueException("\"value\" must be either a string or a map");
}
setJsonAttributeRecurse(jsonObject, path, value);
}
private Object setJsonAttributeRecurse(Map jsonObject, String path, Object value) {
if (path.length()==0 && value==null) {
return jsonObject; // makeJsonObject called
}
String[] parts = path.split("\\.", 2); // {"a", "b[2].c"}
String segment = parts[0]; // "a", "b[2]", "c"
int index = -1;
if (segment.endsWith("]")) {
// Array
String[] split = segment.split("[\\[\\]]");
segment = split[0]; // "b"
String indexString = split[1]; // "2"
try {
index = Integer.parseInt(indexString);
} catch (NumberFormatException e) {
log.debug("Invalid index '{}' in segment '{}' (ignored)", indexString, segment);
}
}
boolean lastSegment = (parts.length==1);
if (lastSegment && value!=null) {
return jsonObject.put(segment, value);
}
Object currentObject = jsonObject.get(segment); // Either Object, List or Null
if (currentObject==null) {
if (index>-1) {
currentObject = new ArrayList();
} else {
currentObject = makeJsonObject();
}
jsonObject.put(segment, currentObject);
}
if (currentObject instanceof ArrayList) {
if (index<0) {
throw new YadaInvalidValueException("Not an array at {}", segment);
}
List currentList = (List) currentObject;
for (int k = currentList.size(); k <= index; k++) {
// Add missing cells if any
currentList.add(makeJsonObject());
}
currentObject = currentList.get(index);
}
int dotPos = path.indexOf(".");
String remainingPath = dotPos > -1 ? path.substring(dotPos+1) : "";
return setJsonAttributeRecurse((Map)currentObject, remainingPath, value);
}
private Object getJsonPath(Map jsonSource, String objectPath) {
Object result = jsonSource;
String[] parts = objectPath.split("\\.");
for (int i = 0; i < parts.length; i++) {
String segment = parts[i];
int index = -1;
if (segment.endsWith("]")) {
// Array
String[] split = segment.split("[\\[\\]]");
segment = split[0];
String indexString = split[1];
try {
index = Integer.parseInt(indexString);
} catch (NumberFormatException e) {
log.debug("Invalid index '{}' in segment '{}' (ignored)", indexString, parts[i]);
}
}
result = ((Map) result).get(segment);
if (result==null) {
log.debug("Null value at {}", segment);
return null;
}
if (index>-1) { // Array
try {
result = ((List)result).get(index);
} catch (IndexOutOfBoundsException e) {
log.debug("Index out of bounds at {}[{}]", segment, index);
return null;
}
if (result==null) {
log.debug("Null value at {}[{}]", segment, index);
return null;
}
}
}
return result;
}
/**
* Returns a list of user-friendly timezones like "Europe/Rome"
* @return
*/
public List getTimezones() {
if (computedTimezones==null) {
computedTimezones = new ArrayList();
String[] allTimezones = TimeZone.getAvailableIDs();
for (String timezone : allTimezones) {
// Only timezones with a / that start with a continent, like "Europe/Rome"
if (timezone.indexOf('/')>-1 && !timezone.startsWith("Etc/") && !timezone.startsWith("SystemV/")) {
computedTimezones.add(timezone);
}
}
}
return computedTimezones;
}
/**
* Get a list of GMT/UTC time offsets from UTC-12:00 to UTC+14:00
* @param prefix use either "GMT" or "UTC"
* @return from "GMT-12:00" to "GMT+14:00"
*/
public List getTimezoneOffsets(String prefix) {
if (computedTimezoneOffsets==null) {
computedTimezoneOffsets = new ArrayList();
// https://en.wikipedia.org/wiki/List_of_UTC_time_offsets
for (int i=-12; i<=14; i++) {
computedTimezoneOffsets.add(String.format("%s%+03d:00", prefix, i));
if (i==-10 || i==-4 || i==3 || i==4 || i==5 || i==6 || i==9 || i==10) {
computedTimezoneOffsets.add(String.format("%s%+03d:30", prefix, i+(i<0?1:0)));
}
if (i==5 || i==8 || i==12) {
computedTimezoneOffsets.add(String.format("%s%+03d:45", prefix, i));
}
}
}
return computedTimezoneOffsets;
}
/**
* Simple email address syntax check: the format should be [email protected]
* where X does not contain @ and Y does not contain @, nor . at the edges.
* Also no spaces anywhere.
* @param email
* @return
*/
public boolean isEmailValid(String email) {
if (email.indexOf(CHAR_SPACE)>-1) {
return false;
}
int firstAtPos = email.indexOf(CHAR_AT);
int lastAtPos = email.lastIndexOf(CHAR_AT);
if (firstAtPos<0 || firstAtPos != lastAtPos || lastAtPos==email.length()-1) {
return false; // No @ or more than one or one at the end
}
int lastDotPos = email.lastIndexOf(CHAR_DOT);
if (lastDotPos<0 || // No DOT
lastDotPos stringMap) {
String result = stringMap.toString(); // "{n1=v1, n2=v2}"
result = StringUtils.chop(result); // Remove }
result = StringUtils.removeStart(result, "{"); // Remove {
return result;
}
/**
* Returns a random integer number
* @param minIncluded minimum value, included
* @param maxIncluded maximum value, included
* @return
*/
public int getRandom(int minIncluded, int maxIncluded) {
if (maxIncluded==Integer.MAX_VALUE) {
maxIncluded = Integer.MAX_VALUE - 1; // Needed to prevent overflow of maxExcluded below
}
int maxExcluded = maxIncluded - minIncluded + 1;
return secureRandom.nextInt(maxExcluded) + minIncluded;
}
/**
* Given the instance of a "specific" class created specifying a single type T while extending a generic class,
* retrieve the class of the type T.
* It also works when looking for the generic super-super class at any hierarchy level.
* Example:
* the generic class is public abstract class Shape {...}
* the specific class is public class Circle extends Shape
* the instance is new Circle()
* the returned value is Red.class
* @param specificClassInstance instance of the specific class, usually "this" when called from inside either the specific or the generic abstract class.
* @return the class T used to make the generic specific, or null if there is no generic superclass in the hierarchy
*/
public Class> findGenericClass(Object specificClassInstance) {
Class> theClass = specificClassInstance.getClass();
while (theClass!=null && !(theClass.getGenericSuperclass() instanceof ParameterizedType)) {
theClass = theClass.getSuperclass();
}
if (theClass!=null) {
return (Class>)((ParameterizedType)theClass.getGenericSuperclass()).getActualTypeArguments()[0];
}
return null;
}
/**
* Merges all files matched by a pattern, in no particular order.
* @param sourceFolder root folder where files are to be found
* @param sourceFilePattern regex pattern to match files, e.g. ".*.js"
* @param outputFile file that will contain the joined files
* @param depth (optional) max depth of folders: null or 1 for no recursion
* @param deleteSource (optional) Boolean.TRUE to attempt deletion of source files
* @throws IOException
*/
public void joinFiles(Path sourceFolder, String sourceFilePattern, File outputFile, Integer depth, Boolean deleteSource) throws IOException {
depth = depth==null ? 1 : depth; // By default we don't look into subfolders
FileOutputStream joinedStream = new FileOutputStream(outputFile);
Iterator allFilesIter = java.nio.file.Files.find(sourceFolder, depth, (path, basicFileAttributes) -> path.toFile().getName().matches(sourceFilePattern)).iterator();
while (allFilesIter.hasNext()) {
Path filePath = allFilesIter.next();
com.google.common.io.Files.copy(filePath.toFile(), joinedStream);
if (Boolean.TRUE.equals(deleteSource)) {
filePath.toFile().delete();
}
}
joinedStream.close();
}
/**
* Creates a folder in the system temp folder. The name is prefixed with "yada".
* @return
* @throws IOException
* @see {@link Files#createTempDirectory(String, java.nio.file.attribute.FileAttribute...)}
*/
@Deprecated // This should not be used because the operation is not atomic
// Use Files.createTempDirectory() instead
public File makeTempFolder() throws IOException {
File file = File.createTempFile("yada", "");
file.delete();
file.mkdir();
return file;
}
/**
* Finds the path between two files or folders using forward (unix) slashes as a separator
* @param ancestorFolder
* @param descendantFolder
* @return
*/
public String relativize(File ancestorFolder, File descendantFolder) {
return relativize(ancestorFolder.toPath(), descendantFolder.toPath());
}
/**
* Finds the path between two files or folders using forward (unix) slashes as a separator.
* It works even if the two arguments point to places in different trees.
* @param ancestorPath the root path e.g. "/a/b"
* @param descendantPath the final path e.g. "/a/b/c/the.gif"
* @return the relative path from ancestorPath to descendantPath e.g. "c/the.gif"
*/
public String relativize(Path ancestorPath, Path descendantPath) {
// When on windows, if one path has a drive letter and the other doesn't, an exception would be thrown, so we fix that.
if (ancestorPath.isAbsolute() && !descendantPath.isAbsolute()) {
descendantPath = Paths.get(ancestorPath.getRoot().toString(), descendantPath.toString());
} else if (!ancestorPath.isAbsolute() && descendantPath.isAbsolute()) {
ancestorPath = Paths.get(descendantPath.getRoot().toString(), ancestorPath.toString());
}
String segment = ancestorPath.relativize(descendantPath).toString();
return segment.replaceAll("\\\\", "/");
}
/**
* Split an HTML string in two parts, not breaking words, handling closing and reopening of html tags.
* Useful when showing some part of a text and the whole of it after a user clicks.
* For example, the string "<p>Some text here</p> becomes ["<p>Some text</p>","<p>here</p>"].
* The HTML is not splitted exactly at splitPos if there's a word there, a tag, or if the paragraph ends in the next 20 characters:
* in such cases the split position is increased accordingly.
* Note: does not work in any possible scenario. For example <ul><li> is not split properly because it creates two list entries
* if the split point is inside the li. Tag attributes are not currently handled properly.
* @param htmlToSplit The html text to split, must be well-formed (all opened tags must be closed properly)
* @param splitPos the minimum position, in number of characters including tags, where to split
* @return an array of two self-contained html parts, where each opened tag is correctly closed. The second part could be null.
* @see #splitAtWord(String, int)
*/
public String[] splitHtml(String htmlToSplit, int splitPos) {
String[] result = new String[2];
char[] charArray = htmlToSplit.toCharArray();
int maxPos = charArray.length-1;
boolean tag = false; // True when the current character is inside an HTML tag
boolean tagOpen = false; // True when an HTML tag has been opened
int pos = 0;
Stack tagsToCopy = new Stack<>();
StringBuffer tagName = null;
try {
while (pos') {
// Last character of a tag. If it was an opening tag, add it to the stack of opened tags
tag = false;
if (tagOpen) {
String tagNameString = tagName.toString();
// br is not added because it does not need a closing tag
// TODO what other html tags don't have a closing one?
if (!"br".equals(tagNameString) && !tagNameString.contains("br/")) {
tagsToCopy.add(tagNameString);
}
tagOpen=false;
} else {
// It was a close tag. We presume that it was the same as the last one on the stack and we forget it.
if (!tagsToCopy.isEmpty()) {
tagsToCopy.pop();
}
}
continue;
} else if (pos>=splitPos && !tag && isSpace) {
// We reached or surpassed the split point outside of a tag and at a space character.
// The HTML can be split here, unless there's a closing p in the next 20 character
// TODO 20 should be a parameter?
// TODO should be done for too
int closep = htmlToSplit.indexOf("", pos);
if (closep>-1 && closep-pos<20) {
// Close the paragraph in the first part
pos = closep + "".length();
// Forget all tags up to the opening paragraph because we assume that we skipped the closing ones
while (!tagsToCopy.isEmpty()) {
String tagToCopy = tagsToCopy.pop();
if ("p".equals(tagToCopy)) {
break;
}
}
}
// Split at a safe position
result[0] = htmlToSplit.substring(0, pos);
result[1] = htmlToSplit.substring(pos);
// Add any needed closing tags to the first part and opening tags to the second part
while (!tagsToCopy.isEmpty()) {
String tagToCopy = tagsToCopy.pop();
result[0] += "" + tagToCopy + '>'; // Tag closed in the first part
result[1] = "<" + tagToCopy + '>' + result[1]; // Tag reopened in the second part
}
return result;
} else if (tagOpen) {
tagName.append(current);
}
pos++;
}
} catch (Exception e) {
// In case of error, the whole HTML is returned in the first part, and null in the second
log.error("Can't split HTML (returned whole)", e);
}
result[0] = htmlToSplit;
return result;
}
/**
* Ensure that the given filename has not been already used, by adding a counter.
* For example, if baseName is "dog" and usedNames is {"dog.jpg", "dog_1.jpg", "dog_2.jpg"}, the
* result will be "dog_3.jpg"
* The usedNames array doesn't have to contain identical or sequential baseNames: {"dog.jpg", "cat.jpg", "dog_2.jpg"}
* This version does not check if a file exists on disk. For that, see {@link #findAvailableName(File, String, String, String)}
* This method can be used with any strings, not necessarily filenames: just use null for the extension.
* @param baseName filename to add, without extension
* @param extensionNoDot filename extension without dot, can be empty or null if the extension is not needed
* @param counterSeparator string to separate the filename and the counter, can be empty or null
* @param usedNames filenames used so far, can start empty but never null, and will be modified by adding the new name
* @return the original filename with extension, or a new version with a counter added
* @throws IOException
* @see {@link #findAvailableName(File, String, String, String)}
*/
public String findAvailableFilename(String baseName, String extensionNoDot, String counterSeparator, Set usedNames) throws IOException {
counterSeparator = counterSeparator==null?"":counterSeparator;
String extension = StringUtils.isAllBlank(extensionNoDot) ? "" : "." + extensionNoDot;
String fullName = baseName + extension;
int counter = 0;
long startTime = System.currentTimeMillis();
int timeoutMillis = 10000; // 10 seconds to find a result seems to be reasonable
while (true) {
if (!usedNames.contains(fullName)) {
usedNames.add(fullName);
return fullName;
}
counter++;
fullName = baseName + counterSeparator + counter + extension;
if (System.currentTimeMillis()-startTime > timeoutMillis) {
throw new IOException("Timeout trying to create a unique name starting with " + baseName);
}
}
}
/**
* Check if a date is not more than maxYears years from now, not in an accurate way.
* Useful to check validity of a date coming from the browser.
* @param someDate
* @param maxYears max number of years (positive or negative) for this date to be valid. When null, defaults to 4000 years.
* @return false if the date is too distant from now
*/
public boolean dateValid(Date someDate, Integer maxYears) {
long now = System.currentTimeMillis();
long toCheck = someDate.getTime();
long difference = Math.abs(now-toCheck);
final long millisInYearInaccurate = 365*MILLIS_IN_DAY;
maxYears = maxYears==null?4000:maxYears;
return difference < maxYears*millisInYearInaccurate;
}
/**
* Gets image dimensions for given file, ignoring orientation flag
* @param imageFile image file
* @return dimensions of image, or YadaIntDimension.UNSET when not found
*/
// Adapted from https://stackoverflow.com/a/12164026/587641
// The default jpeg image reader does not handle the exif Orientation flag properly
// so a "vertical" image with an orientation flag of 6 is considered horizontal
// and will have a width larger than the height
// See https://www.impulseadventure.com/photo/exif-orientation.html
public YadaIntDimension getImageDimensionDumb(File imageFile) {
String suffix = getFileExtension(imageFile);
Iterator iter = ImageIO.getImageReadersBySuffix(suffix);
while (iter.hasNext()) {
ImageReader reader = iter.next();
try(ImageInputStream stream = new FileImageInputStream(imageFile)) {
reader.setInput(stream);
int width = reader.getWidth(reader.getMinIndex());
int height = reader.getHeight(reader.getMinIndex());
return new YadaIntDimension(width, height);
} catch (IOException e) {
log.debug("Error reading dimensions for {} using reader {}", imageFile, reader, e);
} finally {
reader.dispose();
}
}
return YadaIntDimension.UNSET;
}
/**
* Gets the image dimensions considering the EXIF orientation flag.
* Remember to use the "-auto-orient" flag of the ImageMagick convert command.
* If the EXIF width and height information is missing, the getImageDimensionDumb() method is called instead.
* See https://www.impulseadventure.com/photo/exif-orientation.html
* @param imageFile
* @return
*/
public YadaIntDimension getImageDimension(File imageFile) {
String suffix = getFileExtension(imageFile);
if (!ImageIO.getImageReadersBySuffix(suffix).hasNext()) {
return YadaIntDimension.UNSET; // Not an image
}
try(InputStream stream = new FileInputStream(imageFile)) {
Metadata metadata = ImageMetadataReader.readMetadata(stream);
// for (com.drew.metadata.Directory directory2 : metadata.getDirectories()) {
// for (com.drew.metadata.Tag tag : directory2.getTags()) {
// if (tag.getTagName().equalsIgnoreCase("Orientation")) {
// System.out.println(tag.getTagName());
// System.out.println(tag);
//
// }
// }
// }
boolean valid = false;
int orientation = 1; // Default when can't be retrieved
int width = -1, height = -1;
ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class);
if (directory!=null && directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) {
orientation = directory.getInt(ExifIFD0Directory.TAG_ORIENTATION);
}
ExifSubIFDDirectory directory2 = metadata.getFirstDirectoryOfType(ExifSubIFDDirectory.class);
if (directory2!=null) {
width = directory2.getInt(ExifSubIFDDirectory.TAG_EXIF_IMAGE_WIDTH);
height = directory2.getInt(ExifSubIFDDirectory.TAG_EXIF_IMAGE_HEIGHT);
valid = true;
} else {
// If there is no exif directory, look for info in the gif header
GifHeaderDirectory gifHeaderDirectory = metadata.getFirstDirectoryOfType(GifHeaderDirectory.class);
if (gifHeaderDirectory!=null) {
width = gifHeaderDirectory.getInt(GifHeaderDirectory.TAG_IMAGE_WIDTH);
height = gifHeaderDirectory.getInt(GifHeaderDirectory.TAG_IMAGE_HEIGHT);
valid = true;
} else {
// Look for info in the jpeg header
JpegDirectory jpegDirectory = metadata.getFirstDirectoryOfType(JpegDirectory.class);
if (jpegDirectory!=null) {
width = jpegDirectory.getInt(JpegDirectory.TAG_IMAGE_WIDTH);
height = jpegDirectory.getInt(JpegDirectory.TAG_IMAGE_HEIGHT);
valid = true;
}
}
}
if (valid) {
if (orientation==6 || orientation==8) {
// Image is rotated 90° so dimensions must be swapped
return new YadaIntDimension(height, width);
}
return new YadaIntDimension(width, height);
}
} catch (Exception e) {
log.debug("Exception reading image dimensions for {}: {}}", imageFile, e.getMessage());
}
log.debug("Fallback to dumb version while reading image dimensions for {}", imageFile);
return getImageDimensionDumb(imageFile);
}
/**
* Returns the current stack trace as a string, formatted on separate lines
* @return
*/
public String getCurrentStackTraceFormatted() {
StringBuilder stringBuilder = new StringBuilder();
for (StackTraceElement element : Thread.currentThread().getStackTrace()) {
stringBuilder.append("\tat ").append(element).append('\n');
}
return stringBuilder.toString();
}
/**
* Returns a random element from the list
* @param list
* @return a random element from the list, or null if the list is empty
*/
public T getRandomElement(List list) {
if (list.size()>0) {
int pos = secureRandom.nextInt(list.size());
return list.get(pos);
}
return null;
}
/**
* Convert from an amount of time to a string in the format xxd:hh:mm:ss
* @param amount interval that needs to be formatted
* @param timeUnit the unit of the interval
* @return a formatted string representing the input interval
*/
public static String formatTimeInterval(long amount, TimeUnit timeUnit) {
long totSeconds = timeUnit.toSeconds(amount);
long seconds = totSeconds % 60;
long totMinutes = timeUnit.toMinutes(amount);
long minutes = totMinutes % 60;
long totHours = timeUnit.toHours(amount);
long hours = totHours % 24;
long days = timeUnit.toDays(amount);
String result = String.format("%02d:%02d", minutes, seconds);
if (hours+days>0) {
result = String.format("%02d:", hours) + result;
if (days>0) {
result = String.format("%dd:", days) + result;
}
}
return result;
}
/**
* Perform autowiring of an instance that doesn't come from the Spring context, e.g. a JPA @Entity.
* Post processing (@PostConstruct etc) is also performed but initialization is not.
* @param instance to autowire
* @see AutowireCapableBeanFactory#autowireBean(Object)
*/
public void autowire(Object instance) {
autowireCapableBeanFactory.autowireBean(instance);
}
/**
* Perform autowiring of an instance that doesn't come from the Spring context, e.g. a JPA @Entity or normal java instance made with new.
* Post processing (@PostConstruct etc) and initialization are also performed.
* Beans from the WebApplicationContext like @Controller are not injected because they don't belong to the root Application Context used here.
* If you need to do that, use {@link YadaWebUtil#autowireAndInitialize(Object)}
* @param instance to autowire
* @return the autowired/initialized bean instance, either the original or a wrapped one
* @see {@link YadaWebUtil#autowireAndInitialize(Object)}, {@link AutowireCapableBeanFactory#autowireBean(Object)}, {@link AutowireCapableBeanFactory#initializeBean(Object, String)}, {@link #autowire(Object)}
*/
public Object autowireAndInitialize(Object instance) {
autowireCapableBeanFactory.autowireBean(instance);
return autowireCapableBeanFactory.initializeBean(instance, instance.getClass().getSimpleName());
}
/**
* Remove a counter that has been added by {@link #findAvailableName}
* @param filename
* @param counterSeparator
* @return
*/
public static String stripCounterFromFilename(String filename, String counterSeparator) {
String[] filenameParts = YadaUtil.splitFileNameAndExtension(filename);
// If the filename has a counter attached, strip it
String prefix = filenameParts[0]; // product_2
int pos = prefix.lastIndexOf(counterSeparator);
if (pos>-1 && pos maxNumber = Arrays.stream(currentFiles)
.filter(file -> pattern.matcher(file.getName()).matches())
.map(file -> {
Matcher matcher = pattern.matcher(file.getName());
matcher.find();
return Integer.parseInt(matcher.group(1));
})
.max(Comparator.naturalOrder());
int nextNumber = maxNumber.map(n -> n + 1).orElse(1);
String filename = (extensionNoDot != null)
? String.format("%s%s%04d.%s", baseName, counterSeparator, nextNumber, extensionNoDot)
: String.format("%s%s%04d", baseName, counterSeparator, nextNumber);
return new File(targetFolder, filename);
}
/**
* Creates an empty file that doesn't already exist in the specified folder
* with the specified leading characters (baseName).
* A counter may be appended to make the file unique.
* This operation is thread safe.
* @param targetFolder the folder where the file has to be placed
* @param baseName the leading characters for the file, like "product"
* @param extension the extension without a dot, like "jpg"
* @param counterSeparator the separator to be used before appending the number, e.g. "_"
* @return a new file in that folder, with a name like "product_2.jpg" or "product.jpg"
* @throws IOException
*/
public static File findAvailableName(File targetFolder, String baseName, String extensionNoDot, String counterSeparator) throws IOException {
String extension = "." + extensionNoDot;
String filename = baseName + extension;
int counter = 0;
long startTime = System.currentTimeMillis();
int timeoutMillis = 10000; // 10 seconds to find a result seems reasonable
while (true) {
File candidateFile = new File(targetFolder, filename);
try {
if (candidateFile.createNewFile()) {
return candidateFile;
}
} catch (IOException e) {
log.error("Can't create file {}", candidateFile);
throw e;
}
counter++;
filename = baseName + counterSeparator + counter + extension;
if (System.currentTimeMillis()-startTime > timeoutMillis) {
throw new IOException("Timeout trying to create a unique file starting with " + baseName + " in folder " + targetFolder);
}
}
}
/**
* Creates a file with a unique filename by appending a number after the specified separator if needed.
* If the targetFile exists already, a new file is created with a proper counter at the end. The counter may be stripped
* altogether (if the original file had a counter and no file without counter exists) or added or incremented.
* The new counter might not be higher than the original one, nor sequential. It depends on what's already on
* the filesystem.
* This operation is thread safe.
* @param targetFile the file that we want to create.
* @param counterSeparator (optional) when null, "_" is used.
* @return a File that doesn't already exist
* @throws IOException
*/
public static File findAvailableName(File targetFile, String counterSeparator) throws IOException {
if (counterSeparator==null) {
counterSeparator="_";
}
String targetFilename = targetFile.getName();
String extension = splitFileNameAndExtension(targetFilename)[1];
String strippedName = stripCounterFromFilename(targetFilename, counterSeparator);
return findAvailableName(targetFile.getParentFile(), strippedName, extension, counterSeparator);
}
/**
* Force initialization of localized strings implemented with Map<Locale, String>.
* It must be called in a transaction.
* @param fetchedEntity object fetched from database that may contain localized strings
* @param targetClass type of fetchedEntity element
*/
public static void prefetchLocalizedStrings(targetClass fetchedEntity, Class> targetClass, String...attributes) {
if (fetchedEntity!=null) {
List list = new ArrayList<>();
list.add(fetchedEntity);
prefetchLocalizedStringList(list, targetClass, attributes);
}
}
/**
* Force initialization of localized strings implemented with Map<Locale, String>.
* It must be called in a transaction.
* @param fetchedEntity object fetched from database that may contain localized strings
* @param targetClass type of fetchedEntities elements
* @param attributes the localized string attributes to prefetch (optional). If missing, all attributes of the right type are prefetched.
*/
public static void prefetchLocalizedStringsRecursive(targetClass fetchedEntity, Class> targetClass, String...attributes) {
if (fetchedEntity!=null) {
List list = new ArrayList<>();
list.add(fetchedEntity);
prefetchLocalizedStringListRecursive(list, targetClass, attributes);
}
}
/**
* Force initialization of localized strings implemented with Map<Locale, String>.
* It must be called in a transaction.
* @param entities objects fetched from database that may contain localized strings
* @param entityClass type of fetchedEntities elements
* @param attributes the localized string attributes to prefetch (optional). If missing, all attributes of the right type are prefetched.
*/
public static void prefetchLocalizedStringListRecursive(List entities, Class> entityClass, String...attributes) {
// TODO I don't actually get how this works.
// It looks like it's prefetching all first-level local strings, then
// if an attribute is not a generic, it will fetch all first-level local strings there.
// It doesn't make sense.
// It should instead recurse on itself for all attributes that are neither primitive not local strings,
// unrolling collections and arrays.
if (entities==null || entities.isEmpty()) {
return;
}
// Prefetch first level strings for all objects
prefetchLocalizedStringList(entities, entityClass, attributes);
// Look for strings in all attributes recursively
ReflectionUtils.doWithFields(entityClass, new ReflectionUtils.FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
for (Object object : entities) {
if (object!=null) {
try {
field.setAccessible(true);
Object fieldValue = field.get(object);
if (fieldValue!=null) {
Class fieldClass = field.getType();
List secondLevel = new ArrayList<>();
secondLevel.add(fieldValue);
// TODO shouldn't this call prefetchLocalizedStringListRecursive()?
prefetchLocalizedStringList(secondLevel, fieldClass, attributes);
}
} catch (Exception e) {
log.error("Failed to initialize field {} for object {} (ignored)", field, object);
}
}
}
}
}, new ReflectionUtils.FieldFilter() {
@Override
public boolean matches(Field field) {
Type type = field.getGenericType();
return !(type instanceof ParameterizedType);
}
});
}
/**
* Force initialization of localized strings implemented with Map<Locale, String>.
* It must be called in a transaction.
* @param entities objects fetched from database that may contain localized strings
* @param entityClass type of fetchedEntities elements
* @param attributes the localized string attributes to prefetch (optional). If missing, all attributes of the right type are prefetched.
*/
public static void prefetchLocalizedStringList(Collection entities, Class> entityClass, String...attributes) {
List attributeNames = Arrays.asList(attributes);
if (entities==null || entities.isEmpty()) {
return;
}
// Look for fields of type Map
ReflectionUtils.doWithFields(entityClass, new ReflectionUtils.FieldCallback() {
@Override
public void doWith(Field field) throws IllegalArgumentException, IllegalAccessException {
for (Object object : entities) {
if (object!=null) {
try {
field.setAccessible(true);
Object fieldValue = field.get(object);
if (fieldValue!=null) {
Method sizeMethod = Map.class.getMethod("size");
sizeMethod.invoke(fieldValue); // Load all the map
}
} catch (NoSuchMethodException | SecurityException | InvocationTargetException e) {
log.error("Failed to initialize field {} for object {} (ignored)", field, object);
}
}
}
}
}, new ReflectionUtils.FieldFilter() {
@Override
public boolean matches(Field field) {
if (attributeNames.size()>0 && !attributeNames.contains(field.getName())) {
return false; // Handle only the specified attributes
}
Type type = field.getGenericType();
if (type instanceof ParameterizedType) {
ParameterizedType parameterizedType = (ParameterizedType) type;
Type[] params = parameterizedType.getActualTypeArguments();
return params.length==2 && Locale.class.equals(params[0]) && String.class.equals(params[1]);
}
return false;
}
});
}
/**
* Returns a string representation of the object as Object.toString() does, even if toString() has been overridden
* @param object
* @return
*/
// Must be static to be used by getLocalValue()
public static String getObjectToString(Object object) {
return object.getClass().getName() + "@" + Integer.toHexString(System.identityHashCode(object));
}
/**
* Returns the localized value from a map of Locale -> String.
* Used in entities with localized string attributes.
* If a default locale has been configured with <locale default='true'>
, then that locale is attempted when
* there is no value (null or "") for the needed locale (and they differ)
* @param LocalizedValueMap
* @return the localized value, or the empty string if no value has been defined and no default locale has been configured
*/
public static String getLocalValue(Map LocalizedValueMap) {
return YadaUtil.getLocalValue(LocalizedValueMap, LocaleContextHolder.getLocale());
}
/**
* Returns the localized value from a map of Locale -> String.
* Used in entities with localized string attributes.
* If a default locale has been configured with <locale default='true'>
, then that locale is attempted when
* there is no value for the needed locale (and they differ)
* @param localizedValueMap
* @param locale the needed locale for the value, can be null for the current request locale
* @return the localized value, or the empty string if no value has been defined and no default locale has been configured
*/
public static String getLocalValue(Map localizedValueMap, Locale locale) {
String result=null;
try {
if (locale==null) {
locale = LocaleContextHolder.getLocale();
}
result = localizedValueMap.get(locale);
} catch (Exception e) {
// By default use a safe plain version of the toString() method
String localizedValueMapName = getObjectToString(localizedValueMap);
try {
localizedValueMapName=localizedValueMap.toString();
} catch (Exception e1) {
// Swallow this exception because that's not the real cause;
}
log.debug("Exception while getting localized value from {} with locale={} (ignored): {}", localizedValueMapName, locale, e.getMessage());
// Keep going
}
if (StringUtils.isEmpty(result) && defaultLocale!=null && !defaultLocale.equals(locale)) {
result = localizedValueMap.get(defaultLocale);
}
return result==null?"":result;
}
/**
* Deletes a file without reporting any errors.
* @param file the file. It could also be an empty foder. Folders containing files are not deleted.
*/
public boolean deleteFileSilently(Path file) {
if (file!=null) {
try {
return Files.deleteIfExists(file);
} catch (Throwable e) {
log.debug("File {} not deleted: " + e.getMessage(), file);
}
}
return false;
}
/**
* Close a closeable ignoring exceptions and null.
* @param closeable the object to close(), can be null
* @return true if closed cleanly (or null), false in case of exception
*/
public boolean closeSilently(Closeable closeable) {
try {
if (closeable!=null) {
closeable.close();
}
return true;
} catch (Exception e) {
log.debug("Closeable exception (ignored)", e.getMessage());
}
return false;
}
public void sleepRandom(long minMilliseconds, long maxMilliseconds) {
sleep(minMilliseconds + (long)(Math.random()*(maxMilliseconds-minMilliseconds)));
}
public void sleep(long milliseconds) {
try {
Thread.sleep(milliseconds);
} catch (InterruptedException e) {
log.debug("Sleep interrupted (ignored)", e);
}
}
/**
* Create a MD5 hash of a string (from http://snippets.dzone.com/posts/show/3686)
* @param clear the source text
* @return
* @throws NoSuchAlgorithmException
*/
public String md5Hash(String clear) throws NoSuchAlgorithmException {
MessageDigest m = MessageDigest.getInstance("MD5");
byte[] data = clear.getBytes();
m.update(data,0,data.length);
BigInteger i = new BigInteger(1,m.digest());
return String.format("%1$032X", i);
}
/**
* Copies an inputStream to an outputStream.
* This method blocks until input data is available, end of file is detected, or an exception is thrown.
* Streams are not closed.
* @param inputStream
* @param outputStream
* @param bufferSize the size in bytes of the temporary buffer to use on the copy loop; null for the default of 4096 bytes.
* Use a small buffer (256) when data is over the internet to prevent timeouts somewhere. Use a big buffer for in-memory or disk operations.
* @param sizeLimit the maximum number of bytes to read (inclusive)
* @return the number of bytes read, or -1 if the sizeLimit has been exceeded.
* @see java.nio.file.Files#copy(Path, Path, java.nio.file.CopyOption...)
* @see org.apache.commons.io.IOUtils#copy(InputStream, OutputStream)
* @throws IOException
*/
public long copyStream(InputStream inputStream, OutputStream outputStream, Integer bufferSize, Long sizeLimit) throws IOException {
long totBytes = 0;
if (inputStream!=null) {
int size = bufferSize==null?4096:bufferSize;
byte[] buffer = new byte[size];
int len;
while ((len = inputStream.read(buffer)) != -1) {
totBytes+=len;
if (sizeLimit!=null && totBytes>sizeLimit) {
return -1;
}
outputStream.write(buffer, 0, len);
}
}
return totBytes;
}
/**
* Get the Field of a given class, even from a superclass but not "nested" in a path
* @param rootClass
* @param attributeName
* @return the Field found or null
* @throws YadaInvalidValueException if attributeName is a path (with a dot in it)
*/
// Probably can be replaced by PropertyUtils.getSimpleProperty() from Commons BeanUtils
public Field getFieldNoTraversing(Class rootClass, String attributeName) {
if (attributeName.indexOf('.')>-1) {
throw new YadaInvalidValueException("Attribute name expected, attribute path found: {}", attributeName);
}
Field field = null;
while (field==null && rootClass!=null) {
try {
field = rootClass.getDeclaredField(attributeName);
} catch (NoSuchFieldException e) {
rootClass = rootClass.getSuperclass();
}
}
return field;
}
/**
* Reflection to get the type of a given field, even nested or in a superclass.
* @param rootClass
* @param attributePath field name like "surname" or even a path like "friend.name"
* @return
* @throws NoSuchFieldException if the field is not found in the class hierarchy
* @throws SecurityException
*/
public Class getType(Class rootClass, String attributePath) throws NoSuchFieldException, SecurityException {
if (StringUtils.isBlank(attributePath)) {
return rootClass;
}
String attributeName = StringUtils.substringBefore(attributePath, ".");
Field field = null;
NoSuchFieldException exception = null;
while (field==null && rootClass!=null) {
try {
field = rootClass.getDeclaredField(attributeName);
} catch (NoSuchFieldException e) {
if (exception==null) {
exception=e;
}
rootClass = rootClass.getSuperclass();
// TODO sometimes the attribute is not in the superclass but in the subclass. How do we get that?
}
}
if (field==null) {
if (exception!=null) {
throw exception;
} else {
throw new NoSuchFieldException("No field " + attributeName + " found in hierarchy");
}
}
Class attributeType = field.getType();
// If it's a list, look for the list type
if (java.util.List.class.equals(attributeType) || java.util.Map.class.equals(attributeType)) {
// TODO check if the attributeType is an instance of java.util.Collection
ParameterizedType parameterizedType = (ParameterizedType)field.getGenericType();
if (parameterizedType!=null) {
Type[] types = parameterizedType.getActualTypeArguments();
if (types.length>0) {
if (java.util.Map.class.equals(attributeType)) {
// For maps, skip the key path element
attributePath = StringUtils.substringAfter(attributePath, ".");
}
// For Maps, we get the type of the value
attributeType = (Class>) types[types.length-1];
}
}
}
return getType(attributeType, StringUtils.substringAfter(attributePath, "."));
}
/**
* Ritorna un messaggio localizzato da un contesto fuori da Spring (ad esempio in un enum o un Entity)
* @param key
* @param params
* @return
*/
public static String getMessage(String key, @Nullable Object ... params) {
return messageSource.getMessage(key, params, LocaleContextHolder.getLocale());
}
/**
* Create an instance of a class that belongs to the same package of some given class
* @param anyClassInPackage a class that is in the same package of the one to instantiate (it could be the one to instantiate)
* @param simpleClassName the simple name of the class to instantiate, like "UserProfile"
* @return
*/
public Object getNewInstanceSamePackage(Class anyClassInPackage, String simpleClassName) {
String packageString = anyClassInPackage.getPackage().getName();
String fullClassName = packageString + "." + simpleClassName;
Class objectClass = null;
try {
objectClass = Class.forName(fullClassName);
return objectClass.newInstance();
} catch (ClassNotFoundException e) {
log.error("Class {} not found in package {}", simpleClassName, packageString, e);
throw new YadaInternalException("Class not implemented: " + simpleClassName);
} catch (Exception e) {
log.error("Instantiation error for class {}", objectClass, e);
throw new YadaInternalException("Error while creating instance of " + fullClassName);
}
}
/**
* Return all the classes of a given package.
* @param thePackage
* @return
* @see http://stackoverflow.com/a/21430849/587641
*/
public static List getClassesInPackage(Package thePackage) {
List result = new ArrayList<>();
ClassPathScanningCandidateComponentProvider provider = new ClassPathScanningCandidateComponentProvider(false);
provider.addIncludeFilter(new RegexPatternTypeFilter(Pattern.compile(".*")));
final Set classes = provider.findCandidateComponents(thePackage.getName());
for (BeanDefinition bean: classes) {
try {
result.add(Class.forName(bean.getBeanClassName()));
} catch (ClassNotFoundException e) {
log.debug("Class not found for bean {} (ignored)", bean);
}
}
return result;
}
/**
* Get any bean defined in the Spring ApplicationContext
* @param beanClass
* @param args constructor arguments, can be null or not present
* @return
*/
public static T getBean(Class beanClass, Object ... args) {
String beanName = StringUtils.uncapitalize(beanClass.getSimpleName());
return (T) getBean(beanName, args);
}
/**
* Get any bean defined in the Spring ApplicationContext
* @param nameInApplicationContext the Class.getSimpleName() starting lowercase, e.g. "processController"
* @return
*/
public static Object getBean(String nameInApplicationContext, Object ... args) {
if (applicationContext!=null) {
return applicationContext.getBean(nameInApplicationContext, args);
}
log.debug("No applicationContext injected in getBean() yet - returning null for '{}'", nameInApplicationContext);
return null;
}
/**
* Get any bean defined in the Spring ApplicationContext
* @param nameInApplicationContext the Class.getSimpleName() starting lowercase, e.g. "processController"
* @return
*/
public static Object getBean(String nameInApplicationContext) {
if (applicationContext!=null) {
return applicationContext.getBean(nameInApplicationContext);
}
log.debug("No applicationContext injected in getBean() yet - returning null for '{}'", nameInApplicationContext);
return null;
}
/**
* Ritorna la data nel passato per il numero di minuti indicati
* @param days numero di minuti fa
* @return
*/
public Date minutesAgo(int minuti) {
return new Date(System.currentTimeMillis() - minuti*MILLIS_IN_MINUTE);
}
/**
* Ritorna la data nel passato per il numero di giorni indicati
* @param days numero di giorni fa
* @return
*/
public Date daysAgo(int days) {
return new Date(System.currentTimeMillis() - days*MILLIS_IN_DAY);
}
/**
* Delete all files in a folder that have the specified prefix
* @param folder
* @param prefix
* @return true if all files have been deleted, false if at least one file has not been deleted
*/
public boolean deleteAll(File folder, String prefix) {
File[] files = folder.listFiles((dir1, name) -> (prefix == null || name.startsWith(prefix)));
if (files == null) {
throw new YadaInvalidUsageException("Not a folder or I/O error while deleting files in {}", folder);
}
boolean deletedAll = true;
for (File file : files) {
try {
if (!file.delete()) {
log.debug("File {} not deleted", file);
deletedAll = false;
}
} catch (Exception e) {
log.debug("File {} not deleted: " + e.getMessage(), file);
deletedAll = false;
}
}
return deletedAll;
}
/**
* Delete a file ignoring errors.
* @param file the file to delete, can be null
* @return true if the file has been deleted, false if the file didn't exist or was null or in case of exception
*/
public boolean deleteSilently(File file) {
if (file!=null) {
try {
return file.delete();
} catch (Exception e) {
log.debug("File {} not deleted: " + e.getMessage(), file);
}
}
return false;
}
/**
* Deleted a folder only when empty
* @param folder
* @return true if deleted, false otherwise
*/
public boolean deleteIfEmpty(Path folder) {
try(DirectoryStream directoryStream = Files.newDirectoryStream(folder)) {
if (!directoryStream.iterator().hasNext()) {
Files.delete(folder);
return true;
}
} catch (final Exception e) { // empty
}
return false;
}
/**
* Removes files from a folder starting with the prefix (can be an empty string)
* The folder itself is not removed.
* @param folder
* @param prefix the initial part of the filename or "" for any file
* return the number of deleted files
*/
public int cleanupFolder(Path folder, String prefix) {
int result = 0;
try (DirectoryStream directoryStream = Files.newDirectoryStream(folder, prefix + "*")) {
for (final Path item : directoryStream) {
Files.delete(item);
result++;
}
} catch (final Exception e) {
log.info("Exception while cleaning folder {}", folder, e);
}
return result;
}
/**
* Removes files from a folder starting with the prefix (can be an empty string) and older than the given date.
* The folder itself is not removed.
* @param folder
* @param prefix
* @param olderThan
* @return the number of deleted files
*/
public int cleanupFolder(Path folder, String prefix, Date olderThan) {
int result = 0;
try (DirectoryStream directoryStream = Files.newDirectoryStream(folder, prefix + "*")) {
for (final Path item : directoryStream) {
if (Files.getLastModifiedTime(item).toMillis()>olderThan.getTime()) {
Files.delete(item);
result++;
}
}
} catch (final Exception e) {
log.info("Exception while cleaning folder {}", folder, e);
}
return result;
}
/**
* Returns the file name given the file path
* @param fileWithPath
* @return
*/
public String getFileNoPath(String fileWithPath) {
if (StringUtils.trimToNull(fileWithPath)==null) {
return null;
}
File file = new File(fileWithPath);
return file.getName();
}
/**
* Splits a filename in the prefix and the extension parts. If there is no extension, the second array cell is the empty string
* @param filename
* @return an array with [ filename without extension, extension without dot]
*/
public static String[] splitFileNameAndExtension(String filename) {
String[] result = new String[] {"", ""};
if (!StringUtils.isBlank(filename)) {
int dotpos = filename.lastIndexOf('.');
if (dotpos>-1) {
result[0] = filename.substring(0, dotpos);
if (filename.length()>dotpos+1) {
result[1] = filename.substring(dotpos+1);
}
} else {
result[0] = filename;
}
}
return result;
}
/**
* Da un nome tipo abcd.JPG ritorna "jpg"
* @param filename
* @return l'estensione, oppure null
*/
public String getFileExtension(String filename) {
String result = null;
if (!StringUtils.isBlank(filename)) {
int dotpos = filename.lastIndexOf('.');
if (dotpos>-1 && filename.length()>dotpos+1) {
result = filename.substring(dotpos+1).toLowerCase();
}
}
return result;
}
public String getFileExtension(File file) {
return getFileExtension(file.getName());
}
public int shellExec(String command, List args, Map substitutionMap, ByteArrayOutputStream outputStream) throws IOException {
return shellExec(command, args, substitutionMap, outputStream, 60); // Default timeout is 60 seconds
}
/**
* Run an external shell command.
* @param command the shell command to run, without parameters
* @param args optional command line parameters. Can be null for no parameters. Each parameter can have spaces without delimiting quotes.
* @param optional substitutionMap key-value of placeholders to replace in the parameters. A placeholder is like ${key}, a substitution
* pair is like "key"-->"value". If the value is a collection, arguments are unrolled so key-->collection will result in key0=val0 key1=val1...
* @param timeoutSeconds timeout in seconds after which ExecuteException is thrown. Use -1 for default timeout of 60 seconds and 0 for infinite timeout
* @param optional outputStream ByteArrayOutputStream that will contain the command output (out + err)
* @return the command exit value (maybe not)
* @throws org.apache.commons.exec.ExecuteException when the exit value is 1 or the timeout is triggered
* @throws IOException
*/
public int shellExec(String command, List args, Map substitutionMap, ByteArrayOutputStream outputStream, int timeoutSeconds) throws IOException {
if (outputStream==null) {
// The outputstream is needed so that execution does not block. Will be discarded.
outputStream = new ByteArrayOutputStream();
}
timeoutSeconds = timeoutSeconds<0?60:timeoutSeconds;
ExecuteWatchdog watchdog = new ExecuteWatchdog(timeoutSeconds==0?ExecuteWatchdog.INFINITE_TIMEOUT:timeoutSeconds*1000);
try {
CommandLine commandLine = new CommandLine(command);
if (args!=null) {
Pattern keyPattern = Pattern.compile("\\$\\{([^}]+)}"); // ${PARAMNAME}
for (String arg : args) {
boolean added=false;
// Convert collections to multiple arguments when needed
if (substitutionMap!=null) {
Matcher m = keyPattern.matcher(arg);
if (m.find()) {
String key = m.group(1); // PARAMNAME
Object values = substitutionMap.get(key);
if (values instanceof Collection) {
// The parameter had a collection in the substitution map
int countArg=0;
added=true;
for (Object extractedValue : (Collection)values) {
String newKey = key + countArg; // PARAMNAME0
String newArg = "${" + newKey + "}"; // ${PARAMNAME0}
// The original parameter is replaced with a new indexed parameter and its value is added to the substitution map
commandLine.addArgument(newArg, false); // Don't handle quoting
substitutionMap.put(newKey, extractedValue);
countArg++;
}
}
}
}
if (!added) {
// Add a parameter that didn't have a collection parameter in it
commandLine.addArgument(arg, false); // Don't handle quoting
}
}
}
if (log.isDebugEnabled() && substitutionMap!=null) {
for (Object keyObj : substitutionMap.keySet()) {
String key = (String) keyObj;
log.debug("{}={}", key, substitutionMap.get(key));
if (key.startsWith("{") || key.startsWith("${")) { // Checking { just for extra precaution
log.error("Invalid substitution {}: should NOT start with ${", key);
}
}
}
if (substitutionMap!=null) {
commandLine.setSubstitutionMap(substitutionMap);
}
DefaultExecutor executor = new DefaultExecutor();
// Kill after timeoutSeconds, defaults to 60. 0 means no timeout
executor.setWatchdog(watchdog);
// Output and Error go together
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, outputStream);
executor.setStreamHandler(streamHandler);
log.debug("Executing shell command: {} {}", command, StringUtils.join(args, " "));
int exitValue = executor.execute(commandLine);
if (exitValue!=0) {
log.error("Shell command exited with {}", exitValue);
}
log.debug("Shell command output: \"{}\"", outputStream.toString());
return exitValue;
} catch (IOException e) {
log.error("Shell command output: \"{}\"", outputStream.toString());
log.error("Failed to execute shell command: {} {} {}", command, args!=null?args.toArray():"", substitutionMap!=null?substitutionMap:"", e);
if (watchdog.killedProcess()) {
log.error("Process was killed by watchdog after {} seconds timeout", timeoutSeconds);
}
throw e;
} finally {
closeSilently(outputStream); // This may not be needed
}
}
/**
* Run an external shell command without keyword substitution in the parameters.
* @param command the shell command to run, without parameters
* @param args command line literal parameters. Can be null for no parameters. Each parameter can have spaces without delimiting quotes.
* @param optional outputStream ByteArrayOutputStream that will contain the command output (out + err)
* @return the command exit value
* @throws IOException
*/
public int shellExec(String command, List args, ByteArrayOutputStream outputStream) throws IOException {
return shellExec(command, args, null, outputStream);
}
private String getExecutable(String shellCommandKey) {
boolean mac = SystemUtils.IS_OS_MAC;
boolean linux = SystemUtils.IS_OS_LINUX;
boolean windows = SystemUtils.IS_OS_WINDOWS;
String executable = mac ? config.getString(shellCommandKey + "/executable[@mac='true']") :
linux ? config.getString(shellCommandKey + "/executable[@linux='true']") :
windows ? config.getString(shellCommandKey + "/executable[@windows='true']") : null;
if (executable==null) {
executable = config.getString(shellCommandKey + "/executable[not(@mac) and not(@linux) and not(@windows)]"); // Fallback to generic OS
}
return executable;
}
/**
* Run an external shell command that has been defined in the configuration file.
* The command must be as in the following example:
*
<imageConvert timeoutseconds="20">
<executable windows="true">magick</executable>
<executable mac="true" linux="true">/usr/local/bin/magick</executable>
<arg>convert</arg>
<arg>${FILENAMEIN}</arg>
<arg>${FILENAMEOUT}</arg>
</imageConvert>
*
* Be aware that args can not contain "Commons Configuration variables" because they clash with placeholders as defined below.
* See the yadaframework documentation for full syntax.
* @param shellCommandKey xpath key of the shell command, e.g. "config/shell/cropImage"
* @param substitutionMap optional key-value of placeholders to replace in the parameters. A placeholder is like ${key}, a substitution
* pair is like "key"-->"value". If the value is a collection, arguments are unrolled so key-->collection will result in key0=val0 key1=val1...
* @return the command exit value
* @throws IOException
*/
public int shellExec(String shellCommandKey, Map substitutionMap) throws IOException {
return shellExec(shellCommandKey, substitutionMap, null);
}
/**
* Run an external shell command that has been defined in the configuration file.
* The command must be as in the following example:
*
<imageConvert timeoutseconds="20">
<executable windows="true">magick</executable>
<executable mac="true" linux="true">/usr/local/bin/magick</executable>
<arg>convert</arg>
<arg>${FILENAMEIN}</arg>
<arg>${FILENAMEOUT}</arg>
</imageConvert>
*
* See the yadaframework documentation for full syntax.
* @param shellCommandKey xpath key of the shell command, e.g. "config/shell/cropImage"
* @param substitutionMap optional key-value of placeholders to replace in the parameters. A placeholder is like ${key}, a substitution
* pair is like "key"-->"value". If the value is a collection, arguments are unrolled so key-->collection will result in key0=val0 key1=val1...
* @param outputStream optional ByteArrayOutputStream that will contain the command output (out + err)
* @return the command exit value
* @throws IOException
*/
public int shellExec(String shellCommandKey, Map substitutionMap, ByteArrayOutputStream outputStream) throws IOException {
String executable = getExecutable(shellCommandKey);
// NO Need to use getProperty() to avoid interpolation on ${} arguments
List args = config.getConfiguration().getList(String.class, shellCommandKey + "/arg", null);
// Object argsObject = config.getConfiguration().getProperty(shellCommandKey + "/arg");
// List args = new ArrayList<>();
// if (argsObject!=null) {
// if (argsObject instanceof List) {
// args.addAll((Collection extends String>) argsObject);
// } else {
// args.add((String) argsObject);
// }
// }
// List interpolatedArgs = new ArrayList<>();
// int pos=1;
// for (String arg : args) {
// String interpolatedArg = arg;
// if (arg.contains("${")) {
// interpolatedArg = config.getConfiguration().getString(shellCommandKey + "/arg[" + pos + "]");
// }
// pos++;
// interpolatedArgs.add(interpolatedArg);
// }
Integer timeout = config.getInt(shellCommandKey + "/@timeoutseconds", -1);
return shellExec(executable, args, substitutionMap, outputStream, timeout);
}
/**
* Esegue un comando di shell
* @param command comando
* @param args lista di argomenti (ogni elemento puo' contenere spazi), puo' essere null
* @param substitutionMap key-value of placeholders to replace in the command. A placeholder in the command is like ${key}, a substitution
* pair is like "key"-->"value" . If the value is a collection, arguments are unrolled.
* @param outputStream ByteArrayOutputStream che conterrà l'output del comando (out + err)
* @return the error message (will be empty for a return code >0), or null if there was no error
*/
@Deprecated // use shellExec() instead
public String exec(String command, List args, Map substitutionMap, ByteArrayOutputStream outputStream) {
int exitValue=1;
ExecuteWatchdog watchdog = new ExecuteWatchdog(60000); // Kill after 60 seconds
try {
CommandLine commandLine = new CommandLine(command);
if (args!=null) {
Pattern keyPattern = Pattern.compile("\\$\\{([^}]+)}"); // ${PARAMNAME}
for (String arg : args) {
boolean added=false;
// Convert collections to multiple arguments when needed
if (substitutionMap!=null) {
Matcher m = keyPattern.matcher(arg);
if (m.find()) {
String key = m.group(1); // PARAMNAME
Object values = substitutionMap.get(key);
if (values instanceof Collection) {
int countArg=0;
added=true;
for (Object extractedValue : (Collection)values) {
String newKey = key + countArg; // PARAMNAME0
String newArg = "${" + newKey + "}"; // ${PARAMNAME0}
commandLine.addArgument(newArg, false); // Don't handle quoting
substitutionMap.put(newKey, extractedValue);
countArg++;
}
}
}
}
if (!added) {
commandLine.addArgument(arg, false); // Don't handle quoting
}
}
}
if (log.isDebugEnabled()) {
for (Object keyObj : substitutionMap.keySet()) {
String key = (String) keyObj;
log.debug("{}={}", key, substitutionMap.get(key));
if (key.startsWith("{") || key.startsWith("${")) { // Checking { just for extra precaution
log.error("Invalid substitution {}: should NOT start with ${", key);
}
}
}
commandLine.setSubstitutionMap(substitutionMap);
DefaultExecutor executor = new DefaultExecutor();
executor.setWatchdog(watchdog);
// Output and Error go together
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream, outputStream);
executor.setStreamHandler(streamHandler);
log.debug("Executing shell command: {}", StringUtils.join(commandLine, " "));
exitValue = executor.execute(commandLine);
} catch (Exception e) {
log.error("Failed to execute shell command: " + command + " " + args, e);
String message = e.getMessage();
if (watchdog.killedProcess()) {
log.error("Processed killed by watchdog for timeout after 60 seconds");
message += " - timeout after 60 seconds";
}
return message;
}
return (exitValue>0)?"":null;
}
/**
* Esegue un comando di shell
* @param command comando
* @param args lista di argomenti (ogni elemento puo' contenere spazi), puo' essere null
* @param outputStream ByteArrayOutputStream che conterrà l'output del comando
* @return the error message (will be empty for a return code >0), or null if there was no error
*/
@Deprecated // use shellExec() instead
public String exec(String command, List args, ByteArrayOutputStream outputStream) {
int exitValue=1;
try {
CommandLine commandLine = new CommandLine(command);
if (args!=null) {
for (String arg : args) {
commandLine.addArgument(arg, false); // Don't handle quoting
}
}
DefaultExecutor executor = new DefaultExecutor();
PumpStreamHandler streamHandler = new PumpStreamHandler(outputStream);
executor.setStreamHandler(streamHandler);
log.debug("Executing shell command: {}", commandLine);
if (args!=null) {
log.debug("Command args: {}", args.toArray());
}
exitValue = executor.execute(commandLine);
} catch (Exception e) {
log.error("Failed to execute shell command: " + command + " " + args, e);
return e.getMessage();
}
return exitValue>0?"":null;
}
/**
* Esegue il comando configurato
* @param shellCommandKey chiave completa xpath del comando shell da eseguire e.g. "config/shell/processTunableWhiteImage"
* @param substitutionMap key-value of placeholders to replace in the command. A placeholder in the command is like ${key}, a substitution
* pair is like "key"-->"value"
* @return true if successful
*/
@Deprecated // use shellExec() instead
public boolean exec(String shellCommandKey, Map substitutionMap) {
String executable = getExecutable(shellCommandKey);
// Need to use getProperty() to avoid interpolation on ${} arguments
// List args = config.getConfiguration().getList(String.class, shellCommandKey + "/arg", null);
List args = (List) config.getConfiguration().getProperty(shellCommandKey + "/arg");
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
try {
String error = exec(executable, args, substitutionMap, outputStream);
String commandOutput = outputStream.toString();
if (error!=null) {
log.error("Can't execute shell command \"{}\": {} - {}", shellCommandKey, error, commandOutput);
return false;
}
log.debug(commandOutput);
return true;
} finally {
closeSilently(outputStream);
}
}
// /**
// * Esegue il comando configurato
// * @param shellCommandKey chiave completa xpath del comando shell da eseguire e.g. "config/shell/processTunableWhiteImage"
// * @param params mappa nome-valore delle variabili da sostituire nel comando, per esempio "{NAME}"="pippo"
// */
// public boolean exec(String shellCommandKey, Map params) {
// String executable = config.getString(shellCommandKey + "/executable");
// List args = (List) config.getList(shellCommandKey + "/arg", null);
// List newArgs = new ArrayList();
// // Replace degli argomenti, che possono anche non esserci
// Pattern paramPattern = Pattern.compile("\\{\\w+\\}"); // parentesi graffe con dentro [a-zA-Z_0-9]+
// for (String arg : args) {
// Matcher m = paramPattern.matcher(arg);
// while (m.find()) {
// String paramName = m.group();
// String paramValue = params.get(paramName);
// if (paramValue!=null) {
// arg = arg.replace(paramName, paramValue);
// }
// }
// newArgs.add(arg);
// }
// ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// try {
// String error = exec(executable, newArgs, outputStream);
// String commandOutput = outputStream.toString();
// if (error!=null) {
// log.error("Can't execute shell command \"{}\": {} - {}", shellCommandKey, error, commandOutput);
// return false;
// }
// log.debug(commandOutput);
// return true;
// } finally {
// try {
// outputStream.close();
// } catch (Exception e) {
// // ignored
// log.debug("Can't close outputStream (ignored): " + e);
// }
// }
// }
public boolean isCodiceFiscaleValid(String codiceFiscale) {
try {
if (StringUtils.isBlank(codiceFiscale)) {
return false;
}
codiceFiscale = codiceFiscale.trim().toUpperCase();
if (codiceFiscale.length()==16) {
UCheckDigit uCheckDigit = new UCheckDigit(codiceFiscale); // ATTENZIONE deve essere UPPERCASE
return uCheckDigit.controllaCorrettezza();
}
if (codiceFiscale.length()==11) {
UCheckNum uCheckCfNum = new UCheckNum(codiceFiscale);
boolean maybeValid = uCheckCfNum.controllaCfNum();
if (maybeValid) {
String c = uCheckCfNum.trattCfNum();
return !(c.equals("2") || c.equals("5"));
}
}
} catch (Exception e) {
log.error("Errore di validazione del codice fiscale {}", codiceFiscale, e);
}
return false;
}
/**
* Trova l'eccezione che ha causato questa, nella catena delle eccezioni
* @param exception eccezione conseguente
* @return throwable originario, preso con getCause()
*/
public Throwable getRootException(Throwable exception) {
Throwable root = exception;
for (int i=0; i<50; i++) { // Al massimo 50, per non andare in loop
Throwable cause = root.getCause();
if (cause==null) {
break;
}
root = cause;
}
return root;
}
/**
* Spezza una stringa in due, circa al carattere splitPoint, ma a fine parola.
*/
public String[] splitAtWord(String value, int splitPoint) {
try {
int maxLen = value.length();
while (value.charAt(splitPoint)!=' ' && splitPoint length){
StringBuffer sb = new StringBuffer(string);
int newLen = length - elen;
if (breakAtWord) {
int endIndex = sb.indexOf(" ", newLen);
if (endIndex>-1) {
return sb.insert(endIndex, ellipsis).substring(0, endIndex+elen);
}
// Se lo spazio non c'è evidentemente siamo alla fine della stringa, per cui va presa tutta
return sb.toString();
}
return sb.insert(newLen, ellipsis).substring(0, length);
}
} catch (Exception e) {
log.error("Can't abbreviate '{}' (ignored)", string, e);
}
return string;
}
/**
* Copy (clone) an object via getter/setter.
* The object must implement CloneableFiltered and optionally state what fields should be excluded and left null.
* The id is always excluded.
* All collections are recreated with the same instances unless their classes implement CloneableDeep, in which case the instances are copied with this same method.
* All objects are shallow copied unless they implement CloneableDeep, in which case they are copied with this same method.
* Map keys are never cloned.
*
* NOTE: the object doesn't have to be an @Entity, despite the method name
* NOTE: collection fields of an @Entity must be initialized to an empty instance in the class, or they won't be cloned
* NOTE: any @Entity in the hierarchy is cloned without id and should be explicitly persisted after cloning unless there's PERSIST propagation.
* NOTE: this method works quite well and should be trusted to copy even complex hierarchies.
* NOTE: a transaction should be active to copy entities with lazy associations
*
* Questo metodo crea la copia di un oggetto TRAMITE I SUOI GETTER (anche privati), facendo in modo che alcune collection/mappe vengano copiate pur restando indipendenti.
* In pratica le collection/mappe sono ricreate come istanze nuove con i medesimi oggetti di quelle originali.
* Questo permette di condividere gli oggetti tra le copie, ma di mantenere le associazioni slegate.
* Così se copio un Prodotto, mi trovo gli stessi componenti dell'originale (gli Articolo sono gli stessi) ma posso in seguito toglierli/aggiungerli
* senza influire sull'altra istanza da cui son partito a copiare.
*
* E' possibile specificare quali attributi non copiare grazie all'interfaccia CloneableFiltered. La id non è mai copiata.
* Gli attributi senza getter/setter non sono copiati a prescindere dal filtro.
* Per esempio se l'oggetto che si vuole copiare ha l'attributo pippo e l'attributo pluto, si può fare in modo che sia copiato solo pippo e non pluto (che rimane quindi null).
*
* Se una collection/mappa deve essere clonata anche nel contenuto, i suoi elementi devono implementare CloneableDeep (vedi LocalString).
* Per esempio se l'attributo pippo è una collection di oggetti Cane che non implementa ClonableDeep, nella copia verrà creata una nuova collection
* di oggetti Cane che saranno gli stessi della collection di partenza. Se invcece Cane implementa ClonableDeep, allora gli oggetti Cane contenuti
* nella copia di pippo sono essi stessi delle copie che seguono le stesse regole qui indicate.
*
* ATTENZIONE:
* - da verificare se gli attributi dei parent sono duplicati pure loro
*
* @param source
* @return
*/
// why not use SerializationUtils.clone(..) of commons-lang?
// This is many times slower than writing clone methods by hand on all objects in your object graph.
// However, for complex object graphs, or for those that don't support deep cloning this can be a simple alternative implementation. Of course all the objects must be Serializable.
public static Object copyEntity(CloneableFiltered source) {
return copyEntity(source, false);
}
/**
*
* @param source
* @param setFieldDirectly true to copy using fields and not getter/setter
* @return
*/
public static Object copyEntity(CloneableFiltered source, boolean setFieldDirectly) {
return copyEntity(source, null, setFieldDirectly);
}
/**
* Questo metodo crea la copia di un oggetto TRAMITE I SUOI GETTER (anche privati), facendo in modo che alcune collection/mappe vengano copiate pur restando indipendenti.
* In pratica le collection/mappe sono ricreate come istanze nuove con i medesimi oggetti di quelle originali.
* Questo permette di condividere gli oggetti tra le copie, ma di mantenere le associazioni slegate.
* Così se copio un Prodotto mi trovo gli stessi componenti dell'originale (gli Articolo sono gli stessi) ma posso in seguito toglierli/aggiungerli
* senza influire sull'altra istanza da cui son partito a copiare.
*
* E' possibile specificare quali attributi non copiare grazie all'interfaccia CloneableFiltered. La id non è mai copiata.
* Per esempio se l'oggetto che si vuole copiare ha l'attributo pippo e l'attributo pluto, si può fare in modo che sia copiato solo pippo e non pluto (che rimane quindi null).
*
* Se una collection/mappa deve essere clonata anche nel contenuto, i suoi elementi devono implementare CloneableDeep (vedi LocalString).
* Per esempio se l'attributo pippo è una collection di oggetti Cane che non implementa ClonableDeep, nella copia verrà creata una nuova collection
* di oggetti Cane che saranno gli stessi della collection di partenza. Se invcece Cane implementa ClonableDeep, allora gli oggetti Cane contenuti
* nella copia di pippo sono essi stessi delle copie che seguono le stesse regole qui indicate.
*
* ATTENZIONE:
* - da verificare se gli attributi dei parent sono duplicati pure loro
*
* @param source
* @param classObject class to use to create the new clone when the source is inside a HibernateProxy
* @return
*/
public static Object copyEntity(CloneableFiltered source, Class classObject) {
return copyEntity(source, classObject, false);
}
public static Object copyEntity(CloneableFiltered source, Class classObject, boolean setFieldDirectly) {
Map alreadyCopiedMap = new HashMap<>();
return copyEntity(source, classObject, setFieldDirectly, alreadyCopiedMap, null);
}
/**
*
* @param source the instance to copy
* @param classObject class to use to create the new clone when the source is inside a HibernateProxy
* @param setFieldDirectly false to use getter/setter, true to access the Field directly
* @param yadaAttachedFileCloneSet when not null, all files are copied to a temp folder.
* This is useful when the final path depends on the id
* of a cloned object so it can't be determined during cloning.
* The method yadaAttachedFileCloneSet.moveAll() will have to be called after the clone has been persisted.
* @return
*/
public static Object copyEntity(CloneableFiltered source, Class classObject, boolean setFieldDirectly, YadaAttachedFileCloneSet yadaAttachedFileCloneSet) {
Map alreadyCopiedMap = new HashMap<>();
return copyEntity(source, classObject, setFieldDirectly, alreadyCopiedMap, yadaAttachedFileCloneSet);
}
private static Object copyEntity(CloneableFiltered source, Class classObject, boolean setFieldDirectly, Map alreadyCopiedMap, YadaAttachedFileCloneSet yadaAttachedFileCloneSet) {
if (source==null) {
return null;
}
Class> sourceClass = source.getClass();
// Needed to avoid infinite recursion if a value holds a reference to the parent
Object alreadyCopied = alreadyCopiedMap.get(source);
if (alreadyCopied!=null) {
log.debug("Reusing already copied object {}", alreadyCopied);
return alreadyCopied;
}
try {
// The constructor may be private, so don't just use newInstance()
// Object target = sourceClass.newInstance();
Constructor> constructor = sourceClass.getDeclaredConstructor(new Class[0]);
constructor.setAccessible(true);
Object target = constructor.newInstance(new Object[0]);
if(target instanceof org.hibernate.proxy.HibernateProxy && classObject!=null) {
target = classObject.newInstance();
}
alreadyCopiedMap.put(source, target); // Needed to avoid infinite recursion if a value holds a reference to the parent
copyFields(source, sourceClass, target, setFieldDirectly, alreadyCopiedMap, yadaAttachedFileCloneSet);
Class> superclass = sourceClass.getSuperclass();
while (superclass!=null && superclass!=Object.class) {
sourceClass = superclass;
copyFields(source, sourceClass, target, setFieldDirectly, alreadyCopiedMap, yadaAttachedFileCloneSet);
superclass = sourceClass.getSuperclass();
}
if (isType(sourceClass, YadaAttachedFile.class)) {
// Also copy files on disk for YadaAttachedFiles
target = yadaFileManager.duplicateFiles((YadaAttachedFile) target, yadaAttachedFileCloneSet);
}
return target;
} catch (Exception e) {
String msg = "Can't duplicate object '" + source + "'";
log.error(msg + ": " + e);
throw new YadaInternalException(msg, e);
}
}
/**
* Shallow copy a value either by getter or by field
* @param setFieldDirectly
* @param field
* @param getter
* @param setter
* @param source object containing the value to copy
* @param target object where to copy the value
* @param args optional values to set on the target. When empty, the value is taken from the source.
*/
private static void copyValueShallow(boolean setFieldDirectly, Field field, Method getter, Method setter, Object source, Object target, Object... args) {
try {
if (setFieldDirectly) {
if (args.length==0) {
field.set(target, field.get(source));
} else {
field.set(target, args);
}
} else {
if (args.length==0) {
setter.invoke(target, getter.invoke(source));
} else {
setter.invoke(target, args);
}
}
} catch (Exception e) {
log.debug("Failed to set field {}", field.getName());
}
}
private static void copyProvidedValue(boolean setFieldDirectly, Field field, Method getter, Method setter, Object newValue, Object target) {
try {
if (setFieldDirectly) {
field.set(target, newValue);
} else {
setter.invoke(target, newValue);
}
} catch (Exception e) {
log.debug("Failed to set field {}", field.getName());
}
}
// private static void copyFields(CloneableFiltered source, Class> sourceClass, Object target) {
// copyFields(source, sourceClass, target, false);
// }
/**
* Copy all (not-excluded) fields from the source object to the target clone.
* @param source the object to get fields from
* @param sourceClass
* @param target the object to copy fields to
* @param setFieldDirectly true to bypass the use of getter/setter
* @param alreadyCopiedMap holds all already-cloned objects in order to avoid loops
* @param yadaAttachedFileCloneSet holds all the cloned YadaAttachedFile objects for later copying files on disk
*/
private static void copyFields(CloneableFiltered source, Class> sourceClass, Object target, boolean setFieldDirectly, Map alreadyCopiedMap, YadaAttachedFileCloneSet yadaAttachedFileCloneSet) {
log.debug("Cloning object {} of type {}", getObjectToString(source), sourceClass);
Field[] fields = sourceClass.getDeclaredFields();
// Excluded fields are totally ignored and will be either null or zero (or whatever the default is) in the target object
Field[] excludedFields = source.getExcludedFields(); // See CloneableFiltered.java
ListfilteredFields = excludedFields!=null? (List) Arrays.asList(excludedFields):new ArrayList<>();
for (int i = 0; i < fields.length; i++) {
Field field = fields[i];
field.setAccessible(true);
// Do not copy a field annotated with @YadaCopyNot
boolean skipField = field.isAnnotationPresent(YadaCopyNot.class);
// Also don't copy a static or final field
int modifiers = field.getModifiers();
skipField |= Modifier.isStatic(modifiers) || Modifier.isFinal(modifiers);
// Also don't copy id of entities
skipField |= "id".equals(field.getName()) && sourceClass.isAnnotationPresent(Entity.class);
if (skipField || filteredFields.contains(field)) {
log.debug("Skipping field {}", field.getName());
continue; // Skip the filtered fields
}
log.debug("Copying field {}", field.getName());
boolean copyShallow = field.isAnnotationPresent(YadaCopyShallow.class);
try {
// Retrieve public getter/setter methods
Class> fieldType = field.getType();
String prefix = (fieldType==boolean.class || fieldType==Boolean.class)?"is":"get";
String getterName = prefix + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
String setterName = "set" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
try {
Method getter = null;
Method setter = null;
if (!setFieldDirectly) {
try{
getter = sourceClass.getDeclaredMethod(getterName);
} catch(NoSuchMethodException exc){
//per i boolean posso avere il getXXXX anzichè l' isXXXXXXX
if (fieldType==boolean.class || fieldType==Boolean.class){
getterName = "get" + field.getName().substring(0, 1).toUpperCase() + field.getName().substring(1);
getter = sourceClass.getDeclaredMethod(getterName);
} else {
throw exc;
}
}
getter.setAccessible(true);
setter = sourceClass.getDeclaredMethod(setterName, fieldType);
setter.setAccessible(true);
}
if (copyShallow
|| fieldType.isPrimitive()
|| fieldType==Boolean.class
|| fieldType==Integer.class
|| fieldType==Long.class
|| fieldType==Byte.class
|| fieldType==Character.class
|| fieldType==Short.class
|| fieldType==Float.class
|| fieldType==Double.class
) {
Object[] clonedAncestor = new Object[]{};
if (copyShallow) {
// Check if the shallow value to copy has already been cloned, in which case we use the clone.
// This allows cloned children to attach to the cloned parent instead of the original parent.
// It works for parent of parent etc. at any level.
Object sourceFieldValue = setFieldDirectly ? field.get(source) : getter.invoke(source);
Object clonedFieldValue = alreadyCopiedMap.get(sourceFieldValue);
if (clonedFieldValue!=null) {
clonedAncestor = new Object[]{clonedFieldValue};
}
}
// Just copy
copyValueShallow(setFieldDirectly, field, getter, setter, source, target, clonedAncestor);
// setter.invoke(target, getter.invoke(source));
} else {
if (isType(fieldType, Collection.class)) {
// E' una collection, quindi copio solo i contenuti.
// E' importante che il costruttore del target abbia istanziato il field con una collection vuota.
// Collection sourceCollection = (Collection) getter.invoke(source); // La collection di partenza, serve per i contenuti
// Collection targetCollection = (Collection) getter.invoke(target); // La collection di destinazione da riempire
Collection sourceCollection = setFieldDirectly ? (Collection) field.get(source) : (Collection) getter.invoke(source);
Collection targetCollection = setFieldDirectly ? (Collection) field.get(target) : (Collection) getter.invoke(target);
if (targetCollection==null) {
boolean invalid = false;
try {
Class sourceCollectionClass = sourceCollection.getClass();
if (sourceCollectionClass.getTypeName().startsWith("org.hibernate.collection")) {
invalid = true;
} else {
targetCollection = (Collection) sourceCollectionClass.newInstance();
}
} catch (Exception e) {
log.error("Can't clone collection", e);
invalid = true;
}
if (invalid) {
if (setFieldDirectly) {
throw new YadaInvalidUsageException("The field '{}' on a new instance of {} should not be null but should be an empty collection for cloning", field.getName(), sourceClass);
}
throw new YadaInvalidUsageException("The getter of '{}' on a new instance of {} should not return null but an empty collection for cloning", field.getName(), sourceClass);
}
copyValueShallow(setFieldDirectly, field, getter, setter, source, target, targetCollection);
// The getter should have returned a new empty instance.
// We could
// targetCollection = new ArrayList();
}
// Faccio la copia shallow di tutti gli elementi che non implementano CloneableDeep;
// per questi faccio la copia deep.
for (Object value : sourceCollection) {
if (isType(value.getClass(), CloneableDeep.class)) {
Object clonedValue = YadaUtil.copyEntity((CloneableFiltered) value, null, false, alreadyCopiedMap, yadaAttachedFileCloneSet); // deep
// For YadaAttachedFile objects, duplicate the file on disk too
// if (isType(value.getClass(), YadaAttachedFile.class)) {
// clonedValue = yadaFileManager.duplicateFiles((YadaAttachedFile) clonedValue, yadaAttachedFileCloneSet);
// }
int previousSize = targetCollection.size();
targetCollection.add(clonedValue);
if (previousSize==targetCollection.size()) {
// If the target collection didn't grow, it means that it is probably a Set and the element doesn't
// implement a unique hashCode function: it may be using the id field of an Entity for example, that is null now.
log.debug("It looks like you should implement a better .equals() and .hashCode() function in {}", value.getClass());
throw new YadaInvalidUsageException("Cloned collection not growing when cloning: "
+ "the {}.hashCode() function is returning {}", value.getClass(), clonedValue.hashCode());
}
} else {
targetCollection.add(value); // shallow
}
}
// targetCollection.addAll(sourceCollection);
} else if (isType(fieldType, Map.class)) {
Map sourceMap = setFieldDirectly ? (Map) field.get(source) : (Map) getter.invoke(source);
Map targetMap = setFieldDirectly ? (Map) field.get(target) : (Map) getter.invoke(target);
if (targetMap==null) {
// Se il costruttore non istanzia la mappa, ne creo una arbitrariamente di tipo HashMap
targetMap = new HashMap();
copyValueShallow(setFieldDirectly, field, getter, setter, source, target, targetMap);
// setter.invoke(target, targetMap);
}
// Faccio la copia shallow di tutti gli elementi che non implementano CloneableDeep;
// per questi faccio la copia deep.
if (sourceMap!=null) {
for (Object key : sourceMap.keySet()) {
Object value = sourceMap.get(key);
if (isType(value.getClass(), CloneableDeep.class)) {
Object clonedValue = YadaUtil.copyEntity((CloneableFiltered) value, null, false, alreadyCopiedMap, yadaAttachedFileCloneSet); // deep
// For YadaAttachedFile objects, duplicate the file on disk too
// if (isType(value.getClass(), YadaAttachedFile.class)) {
// clonedValue = yadaFileManager.duplicateFiles((YadaAttachedFile) clonedValue, yadaAttachedFileCloneSet);
// }
targetMap.put(key, clonedValue);
} else {
targetMap.put(key, value); // shallow
}
}
}
// targetMap.putAll(sourceMap);
} else {
// No collection nor map
Object fieldSourceValueObject = setFieldDirectly ? field.get(source) : getter.invoke(source);
Object fieldTargetValueObject = setFieldDirectly ? field.get(target) : getter.invoke(target);
if (fieldSourceValueObject==null && fieldTargetValueObject==null) {
continue; // Prevent possible errors later
}
if (isType(fieldType, CloneableDeep.class)) {
// Deep copy
CloneableFiltered fieldValue = (CloneableFiltered) fieldSourceValueObject;
Object clonedValue = YadaUtil.copyEntity(fieldValue, null, setFieldDirectly, alreadyCopiedMap, yadaAttachedFileCloneSet); // deep but detached
// For YadaAttachedFile objects, duplicate the file on disk too
// if (isType(fieldType, YadaAttachedFile.class)) {
// clonedValue = yadaFileManager.duplicateFiles((YadaAttachedFile) clonedValue, yadaAttachedFileCloneSet);
// }
copyValueShallow(setFieldDirectly, field, getter, setter, source, target, clonedValue);
} else if (isType(fieldType, StringBuilder.class)) {
// String builder/buffer is cloned otherwise changes to the original object would be reflected in the new one
StringBuilder fieldValue = (StringBuilder) fieldSourceValueObject;
StringBuilder fieldClone = new StringBuilder(fieldValue.toString());
copyProvidedValue(setFieldDirectly, field, getter, setter, fieldClone, target);
} else if (isType(fieldType, StringBuffer.class)) {
// String builder/buffer is cloned otherwise changes to the original object would be reflected in the new one
StringBuffer fieldValue = (StringBuffer) fieldSourceValueObject;
StringBuffer fieldClone = new StringBuffer(fieldValue.toString());
copyProvidedValue(setFieldDirectly, field, getter, setter, fieldClone, target);
} else {
// Plain object, just copy the reference (no cloning)
copyValueShallow(setFieldDirectly, field, getter, setter, source, target);
}
}
}
} catch (NoSuchMethodException e) {
// Just skip it
// Non loggo perché uscirebbe il log anche in casi giusti
}
} catch (Exception e) {
log.error("Can't copy field {} (ignored)", field, e);
}
}
}
/**
* Check if a class is of a given type, considering superclasses and interfaces (of superclasses)
* @param fieldType
* @param requiredType
* @return
*/
// Ritorna true se fieldType coincide con requiredType o una sua superclass oppure se requiredType � tra le interfacce di fieldType o delle sue superclassi
public static boolean isType(Class> fieldType, Class requiredType) {
boolean found = false;
while (!found && fieldType!=null) {
found = isTypeNoSuperclass(fieldType, requiredType);
fieldType = fieldType.getSuperclass();
}
return found;
}
private static boolean isTypeNoSuperclass(Class> fieldType, Class> requiredType) {
if (fieldType==null) {
return false;
}
if (fieldType.equals(requiredType)) {
return true;
}
Class>[] interfaces = fieldType.getInterfaces();
for (Class> iface : interfaces) {
if (isTypeNoSuperclass(iface, requiredType)) {
return true;
}
}
return false;
}
// /**
// * Copy all fields from the first argument to the second, without modifying the timezone.
// * The purpose is to have the same time in a different timezone.
// * @param fromCal
// * @param toCal
// */
// public void copyFields(Calendar fromCal, Calendar toCal) {
//
// }
/**
* Check if a date is within two dates expressed as month/day, regardless of the year and of the validity of such dates.
* @param dateToCheck for example new GregorianCalendar()
* @param fromMonth 0-based, better use Calendar.JANUARY etc.
* @param fromDayInclusive 1-based
* @param toMonth 0-based, better use Calendar.JANUARY etc.
* @param toDayExcluded 1-based
* @return
*/
public static boolean dateWithin(Calendar dateToCheck, int fromMonth, int fromDayInclusive, int toMonth, int toDayExcluded) {
if (fromMonth<0 || fromMonth>11) {
throw new YadaInvalidUsageException("Month must be in the range 0-11");
}
if (toMonth<0 || toMonth>11) {
throw new YadaInvalidUsageException("Month must be in the range 0-11");
}
if (fromDayInclusive<1 || fromDayInclusive>31) {
throw new YadaInvalidUsageException("Day must be in the range 1-31");
}
if (toDayExcluded<1 || toDayExcluded>31) {
throw new YadaInvalidUsageException("Day must be in the range 1-31");
}
boolean sameYear = fromMonth<=toMonth;
int monthToCheck = dateToCheck.get(Calendar.MONTH);
if (sameYear && (monthToChecktoMonth)) {
return false;
}
if (!sameYear && (monthToChecktoMonth)) {
return false;
}
// The month is within range, keep checking...
int dayToCheck = dateToCheck.get(Calendar.DAY_OF_MONTH);
if ((monthToCheck==fromMonth && dayToCheck=toDayExcluded)) {
return false;
}
return true;
}
/** Ritorna l'ora più vicina nel passato alla data specificata
* @return
*/
@Deprecated // Timezone is important because it could be not aligned to the hour of the default timezone
public Date roundBackToHour(Date date) {
return roundBackToHour(date, TimeZone.getDefault());
}
/**
* Returns the same calendar object aligned to the next hour
* @param calendar
* @return
*/
public Calendar roundForwardToHour(Calendar calendar) {
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
calendar.add(Calendar.HOUR, 1);
return calendar;
}
/**
* Returns the same calendar object aligned to the previous hour
* @param calendar
* @return
*/
public Calendar roundBackToHour(Calendar calendar) {
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar;
}
/**
* Resets minutes to zero
* @param date
* @param timezone
* @return
*/
public Date roundBackToHour(Date date, TimeZone timezone) {
Calendar calendar = new GregorianCalendar(timezone);
calendar.setTime(date);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar.getTime();
}
/**
* Returns the days between two dates.
* It doesn't take into consideration the time component, so the difference between some time yesterday and any other time today
* will always be 1
* @param olderDate
* @param earlierDate
* @return
* @see ZonedDateTime#until(java.time.temporal.Temporal, java.time.temporal.TemporalUnit)
*/
public long daysBetween(ZonedDateTime olderDate, ZonedDateTime earlierDate) {
ZonedDateTime olderDateBack = olderDate.truncatedTo(ChronoUnit.DAYS);
ZonedDateTime earlierDateBack = earlierDate.truncatedTo(ChronoUnit.DAYS);
return ChronoUnit.DAYS.between(olderDateBack, earlierDateBack);
}
/**
* Counts the days interval between two dates. Time component is ignored.
* @param date1
* @param date2
* @return
*/
public int daysDifference(Date date1, Date date2) {
// FIXME non è né efficiente né precisa ma funziona
// Presa da http://stackoverflow.com/questions/13198609/java-calendar-getting-difference-between-two-dates-times-off-by-one
try {
// Non credo sia necessario passare per un formatter: basta azzerare i componenti di ora, minuti etc. usando Calendar
DateFormat formatter= new SimpleDateFormat("MM/dd/yyyy");
String truncatedDateString1 = formatter.format(date1);
Date truncatedDate1 = formatter.parse(truncatedDateString1);
String truncatedDateString2 = formatter.format(date2);
Date truncatedDate2 = formatter.parse(truncatedDateString2);
long timeDifference = truncatedDate2.getTime()- truncatedDate1.getTime();
return Math.abs((int)(timeDifference / (24*60*60*1000)));
} catch (ParseException e) {
// Should never be
log.error("Failed to compute time difference", e);
}
return 0;
}
/**
* Returns the minutes between two dates.
* It is negative when the first argument is earlier than the second.
* @param recentDate
* @param oldDate
* @return
*/
public static long minutesDifference(Date recentDate, Date oldDate) {
return (recentDate.getTime()-oldDate.getTime()) / MILLIS_IN_MINUTE;
}
/**
* Returns the absolute value of the minutes between two dates.
* It will always be positive.
* @param firstDate
* @param secondDate
* @return
*/
public static long minutesDifferenceAbs(Date firstDate, Date secondDate) {
return Math.abs(firstDate.getTime()-secondDate.getTime()) / MILLIS_IN_MINUTE;
}
/**
* Returns the number of milliseconds since midnight
*/
public static long millisSinceMidnight(Calendar calendar) {
long totMillis = calendar.getTimeInMillis();
return totMillis - YadaUtil.roundBackToMidnight(calendar).getTimeInMillis();
}
public static Date roundBackToMidnight(Date date, TimeZone timezone) {
GregorianCalendar calendar = new GregorianCalendar(timezone);
calendar.setTime(date);
return roundBackToMidnight(calendar).getTime();
}
/**
* Create a new calendar rounded back to the start of the day.
* @param calendar the calendar to copy
* @return a new calendar
*/
public static Calendar roundBackToMidnightClone(Calendar source) {
return roundBackToMidnight((Calendar) source.clone());
}
/**
* Rounds back the calendar to the start of the day.
* @param calendar the calendar to change: the parameter will be modified by this method
* @return the input calendar modified.
*/
public static Calendar roundBackToMidnight(Calendar calendar) {
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
return calendar;
}
/**
* Rounds forward the calendar to the end of the day at 23:59:59.999
* @param calendar the calendar to change: the parameter will be modified by this method
* @return the input calendar modified.
*/
public static Calendar roundForwardToAlmostMidnight(Calendar calendar) {
calendar.set(Calendar.HOUR_OF_DAY, 23);
calendar.set(Calendar.MINUTE, 59);
calendar.set(Calendar.SECOND, 59);
calendar.set(Calendar.MILLISECOND, 999);
return calendar;
}
/**
* Returns the last midnight
* @return
*/
public static Calendar getLastMidnight() {
return YadaUtil.roundBackToMidnight(new GregorianCalendar());
}
/**
* Adds or removes the days. The original object is cloned.
* @param calendar
* @param days
* @return
*/
public static Calendar addDaysClone(Calendar source, int days) {
return addDays((Calendar) source.clone(), days);
}
/**
* Adds or removes the days. The original object is modified.
* @param calendar
* @param days
* @return
*/
public static Calendar addDays(Calendar calendar, int days) {
calendar.add(Calendar.DAY_OF_YEAR, days);
return calendar;
}
/**
* Adds or removes the minutes. The original object is modified.
* @param calendar
* @param minutes
* @return
*/
public static Calendar addMinutes(Calendar calendar, int minutes) {
calendar.add(Calendar.MINUTE, minutes);
return calendar;
}
/**
* Aggiunge (o rimuove) i minuti indicati dalla data
* @param date
* @param hours numero di minuti, può essere negativo
* @return
*/
public static Date addMinutes(Date date, int minutes) {
return new Date(date.getTime()+minutes*MILLIS_IN_MINUTE);
}
/**
* Aggiunge (o rimuove) le ore indicate dalla data
* @param date
* @param hours numero di ore, può essere negativo
* @return
*/
public static Date addHours(Date date, int hours) {
return new Date(date.getTime()+hours*MILLIS_IN_HOUR);
}
/**
* Aggiunge (o rimuove) i giorni indicati dalla data
* @param date
* @param days numero di giorni, può essere negativo
* @return
*/
public static Date addDays(Date date, int days) {
return new Date(date.getTime()+days*MILLIS_IN_DAY);
}
/**
* Aggiunge (o rimuove) gli anni indicati dalla data (approssimato)
* @param date
* @param years numero di 365 giorni, può essere negativo
* @return
*/
public static Date addYears(Date date, int years) {
final long millisInYear = MILLIS_IN_DAY * 365;
return new Date(date.getTime()+millisInYear);
}
/**
* Returns true if the two dates are on the same day
* @param a
* @param b
* @return
*/
public boolean sameDay(Date a, Date b) {
Calendar aCalendar = new GregorianCalendar();
Calendar bCalendar = new GregorianCalendar();
aCalendar.setTime(a);
bCalendar.setTime(b);
int aDay = aCalendar.get(Calendar.DAY_OF_YEAR);
int aYear = aCalendar.get(Calendar.YEAR);
int bDay = bCalendar.get(Calendar.DAY_OF_YEAR);
int bYear = bCalendar.get(Calendar.YEAR);
return aDay==bDay && aYear == bYear;
}
/**
* Create a zip of a set of files using an external process. The process must be configured as "config/shell/zipWithRename"
* and should use zip and zipnote (for renaming). See the /YadaWeb/scripts folder for an example.
* @param zipFile the zip file that has to be created
* @param sourceFiles the files to add to the zip
* @param filenames optional names to give to each added file, in order
* @param fixNames when true, any repeated name will be given an incremental number (regardless or renaming)
* and if filenames is provided, the renamed file will be forced to have the same
* extension of the source file (existing extensions will be removed).
* @return true if the zip file has been created
* @throws IOException
* @throws YadaInvalidUsageException when the length of filenames is greater than zero but different from the length of sourceFiles
*/
public boolean createZipProcess(File zipFile, File[] sourceFiles, String[] filenames, boolean fixNames) throws IOException {
if (filenames!=null && filenames.length>0 && filenames.length!=sourceFiles.length) {
throw new YadaInvalidUsageException("When provided, there must be as many filenames as source files");
}
Map params = new HashMap<>();
String shellCommandKey = "config/shell/zipWithRename";
File folder = zipFile.getParentFile(); // We create all temporary files in the same folder of the target zip
File tempZip = java.nio.file.Files.createTempFile(folder.toPath(), "_tmp_", ".zip").toFile();
// The zip file must not exist yet, so delete it
tempZip.delete();
File tempRename = java.nio.file.Files.createTempFile(folder.toPath(), "_tmp_", ".txt").toFile();
// String sourceNames = Arrays.stream(sourceFiles).map(File::getAbsolutePath).collect(Collectors.joining(" "));
// Create the rename file and the source names list
Set addedFilenames = new HashSet<>();
StringBuilder sourceNames = new StringBuilder();
try (BufferedWriter renameWriter = new BufferedWriter(new FileWriter(tempRename))) {
for (int i=0; i !Files.isDirectory(path)).forEach(path -> {
String pathInZip = parentOfFolder.relativize(path).toString();
log.debug("Zipping {} with path {}", path, pathInZip);
ZipEntry zipEntry = new ZipEntry(pathInZip);
try {
zs.putNextEntry(zipEntry);
zs.write(Files.readAllBytes(path));
zs.closeEntry();
} catch (Exception e) {
log.error("Can't create zip file", e);
throw new YadaSystemException("Can't create zip file", e);
}
});
}
}
}
/**
* Create a zip of a list of files.
* An exception is thrown when a source file is not readable.
* Adapted from http://www.exampledepot.com/egs/java.util.zip/CreateZip.html
* @param zipFile zip file to create
* @param sourceFiles files to zip
* @param filenamesNoExtension optional list of names to give to zip entries. The name extension is also optional: it will be taken from the source file
*/
public void createZipFile(File zipFile, File[] sourceFiles, String[] filenamesNoExtension) {
createZipFile(zipFile, sourceFiles, filenamesNoExtension, false);
}
/**
* Create a zip of a list of files.
* Adapted from http://www.exampledepot.com/egs/java.util.zip/CreateZip.html
* @param zipFile zip file to create
* @param sourceFiles files to zip
* @param filenamesNoExtension optional list of names to give to zip entries. The name extension is also optional: it will be taken from the source file
* @param ignoreErrors true to ignore a file error and keep going with the next file
*/
public void createZipFile(File zipFile, File[] sourceFiles, String[] filenamesNoExtension, boolean ignoreErrors) {
byte[] buf = new byte[1024]; // Create a buffer for reading the files
// Create the ZIP file
Set addedFilenames = new HashSet<>();
try (ZipOutputStream out = new ZipOutputStream(new FileOutputStream(zipFile))) {
// Compress the files
for (int i=0; i 0) {
out.write(buf, 0, len);
}
// Complete the entry
out.closeEntry();
} catch (Exception e) {
if (!ignoreErrors) {
log.error("Error while adding file {} to zip {}" , entryName, zipFile.getAbsolutePath(), e);
throw e;
} else {
log.debug("Error while adding file {} to zip {}" , entryName, zipFile.getAbsolutePath(), e);
}
}
}
}
} catch (IOException e) {
log.error("Can't create zip file", e);
throw new YadaSystemException("Can't create zip file", e);
}
}
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
// Tutti i metodi sotto non sono ancora usati (spostarli a mano a mano che si usano)
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
///////////////////////////////////////////////////////////////////////////////////////////////
public static Calendar roundBackToLastMonthStart(Calendar calendar) {
calendar.add(Calendar.MONTH, -1);
return roundBackToMonth(calendar);
}
public static Calendar roundBackToMonth(Calendar calendar) {
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
calendar.set(Calendar.DAY_OF_MONTH, 1);
return calendar;
}
public static Date roundBackToMonth(Date date, TimeZone timezone) {//timezone = TimeZone.getTimeZone("GMT");
GregorianCalendar calendar = new GregorianCalendar(timezone);
calendar.setTime(date);
return roundBackToMonth(calendar).getTime();
}
public static Date roundFowardToMonth(Date date, TimeZone timezone) {//timezone = TimeZone.getTimeZone("GMT");
GregorianCalendar calendar = new GregorianCalendar(timezone);
calendar.setTime(date);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
calendar.set(Calendar.DAY_OF_MONTH, 1);
calendar.add(Calendar.MONTH, 1);
return calendar.getTime();
}
@Deprecated // To be removed from Yada Framework
public static String normalizzaCellulareItaliano(String cellulare) {
if (cellulare==null || cellulare.trim().length()==0) {
return cellulare;
}
cellulare = cellulare.replaceAll(" ", ""); // Tolgo spazi
cellulare = cellulare.replaceAll("\\.", ""); // Tolgo punti
cellulare = cellulare.replaceAll("/", ""); // Tolgo slash
if (cellulare.startsWith("+") || cellulare.startsWith("00")) {
return cellulare;
}
return "+39"+cellulare; // Metto prefisso
}
@Deprecated // To be removed from Yada Framework
public static boolean validaCellulare(String cellulare) {
try {
if (cellulare.startsWith("+")) {
cellulare = cellulare.substring(1); // tolgo il + iniziale
}
Long.parseLong(cellulare);
return true;
} catch (Exception e) {
// Ignored
}
return false;
}
// Trasforma una mappa qualunque in un set ordinato in base ai valori
public static SortedSet> sortByValue(Map data) {
// Uso il giro del TreeSet per sortare in base al value
SortedSet> result = new TreeSet<>(new Comparator>() {
@Override
public int compare(Entry element1, Entry element2) {
return element1.getValue().compareTo(element2.getValue());
}
});
try {
result.addAll(data.entrySet());
} catch (RuntimeException e) {
String msg = "Can't sort map by value";
log.error(msg, e);
throw e;
}
return result;
}
// Trasforma una mappa qualunque in un set ordinato in base alle chiavi
public static SortedSet> sortByKey(Map data) {
// Uso il giro del TreeSet per sortare in base al value
SortedSet> result = new TreeSet<>(new Comparator>() {
@Override
public int compare(Entry element1, Entry element2) {
return element1.getKey().compareTo(element2.getKey());
}
});
try {
result.addAll(data.entrySet());
} catch (RuntimeException e) {
String msg = "Can't sort map by key";
log.error(msg, e);
throw e;
}
return result;
}
/**
* Ritorna un riferimento all'ApplicationContext per l'utilizzo fuori dal Container (ad esempio negli Entity)
* @return
*/
public static ApplicationContext getApplicationContext() {
return YadaUtil.applicationContext;
}
@Autowired
public void setApplicationContext(ApplicationContext applicationContext) {
YadaUtil.applicationContext = applicationContext;
}
/**
* Converte un filename in modo che sia valido sia per il filesystem (unix/dos) sia per il browser.
* E' molto distruttiva in quanto i caratteri non previsti vengono eliminati. Per questo si chiama "reduce" :-)
* Converte anche a lowercase.
* @param originalFilename
* @return un filename safe, dove i caratteri speciali sono scomparsi
* @deprecated Does not produce the same results of all OS
* @see #ensureSafeFilename(String)
*/
@Deprecated // Does not produce the same results of all OS
public static String reduceToSafeFilename(String originalFilename) {
return reduceToSafeFilename(originalFilename, true);
}
/**
* Converte un filename in modo che sia valido sia per il filesystem (unix/dos) sia per il browser.
* E' molto distruttiva in quanto i caratteri non previsti vengono eliminati. Per questo si chiama "reduce" :-)
* Converte anche a lowercase.
* @param originalFilename
* @param toLowercase true for a lowercase name
* @return un filename safe, dove i caratteri speciali sono scomparsi
* @deprecated Does not produce the same results of all OS
* @see #ensureSafeFilename(String, boolean)
*/
@Deprecated // Does not produce the same results of all OS
public static String reduceToSafeFilename(String originalFilename, boolean toLowercase) {
if (originalFilename==null) {
return "null";
}
// If the filename is a path, keep the last portion
// WARNING: this is wrong because different results are produced on different OS
int pos = originalFilename.indexOf(File.separatorChar);
if (pos>-1) {
try {
originalFilename = originalFilename.substring(pos+1);
} catch (Exception e) {
// The name ends with a separator char
log.debug("Name is empty");
return "";
}
}
//originalFilename = YadaWebUtil.removeHtmlStatic(originalFilename);
char[] resultChars = originalFilename.toCharArray();
char[] lowerChars = originalFilename.toLowerCase().toCharArray();
for (int i = 0; i < resultChars.length; i++) {
char c = lowerChars[i]; // test on the lowercase version
if (c==224 || c==225) { // à, á
c='a';
} else if (c==232 || c==233) { // é, è
c='e';
} else if (c==236 || c==237) { // ì, í
c='i';
} else if (c==242 || c==243) { // ò, ó
c='o';
} else if (c==249 || c==250) { // ù, ú
c='u';
} else if (c==167) {
c='s';
} else if (c==' ') {
c='_';
} else if (c!='.' && c!='+' && !Character.isDigit(c) && (c<'a' || c>'z')) {
c='_';
} else {
if (!toLowercase) {
c = resultChars[i]; // Not changed
}
}
// TODO raffinare con altri casi
resultChars[i]=c;
}
return new String(resultChars).replaceAll("__+", "_").replaceAll("--+", "-");
}
/**
* Converts a candidate filename so that it is valid on all operating systems and browsers, if needed, and also to lowercase.
* @param originalFilename the name to process
* @return either the lowercase original string or something similar. It returns "noname" when the originalFilename is blank.
*/
public String ensureSafeFilename(String originalFilename) {
return ensureSafeFilename(originalFilename, true);
}
/**
* Converts a candidate filename so that it is valid on all operating systems and browsers, if needed.
* @param originalFilename the name to process
* @param toLowercase true to convert to lowercase
* @return either the original string or something similar. It returns "noname" when the originalFilename is blank.
*/
public String ensureSafeFilename(String originalFilename, boolean toLowercase) {
if (StringUtils.isBlank(originalFilename)) {
return "noname";
}
// Normalize the filename to decompose characters using NFKD
String normalizedFilename = Normalizer.normalize(originalFilename, Normalizer.Form.NFKD);
// Remove diacritical marks (accents)
String withoutDiacritics = normalizedFilename.replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
// Replace invalid characters with underscores
String safeFilename = withoutDiacritics.replaceAll("[^a-zA-Z0-9._-]", "_");
// Convert to lowercase if needed
if (toLowercase) {
safeFilename = safeFilename.toLowerCase();
}
return safeFilename;
}
}