package de.ppi.fuwesta.spring.mvc.util; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.StringTokenizer; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class puts definition about Urls to Message-Properties, there are the * following constraints: * <ul> * <li>Fieldname starts with "P_" it is a Parameter. It will added with * * {@link #paramsAsMessages(Class[])}</li> * * <li>Fieldname starts with "PG_" it is define a Parameter-Group. It will added * with {@link #paramGroupAsMessages(Class[])}</li> * * <li>All other fields will be added with * {@link #addUrlsAsMessagesWithPositionedParameters()(Class[])} and * {@link #addUrlsAsMessagesWithNamedParameters (Class[])}</li> * </ul> * * If your parameter isn't a String and you use it via positioned parameter you * should use {@link ParamFormat}, which defines a parameter as a integer, but * you can overwrite it. * */ public class UrlDefinitionsToMessages { /** * Praefix for constants which define a parameter. */ private static final String PRAEFIX_PARAMETER = "P_"; /** * Praefix in messagesource-key for constants which define a parameter * group. */ private static final String MESSAGE_SOURCE_PRAEFIX_PARAMETER = "par"; /** * Praefix for constants which define a parameter group. */ private static final String PRAEFIX_PARAMETER_GROUP = "PG_"; /** * Praefix in messagesource-key for constants which define a parameter * group. */ private static final String MESSAGE_SOURCE_PRAEFIX_PARAMETER_GROUP = "pg"; /** * The Logger for the controller. */ private static final Logger LOG = LoggerFactory .getLogger(UrlDefinitionsToMessages.class); /** * Formates the parameter as an int with no characters. */ public static final String INTEGER = ",number,##"; /** * Annotation which defines how a parameter should be formatted. * */ @Target({ ElementType.FIELD }) @Retention(RetentionPolicy.RUNTIME) public static @interface ParamFormat { /** * The value defines the format for message-formatting. Default is * ",number,##". * */ String value() default INTEGER; } /** the classes with url-infos. */ private final Class<?>[] classesWithUrlInfos; /** * the messages. */ private final Properties messages = new Properties(); /** * the format definition. */ private final Map<String, String> formatDefinition = new HashMap<>(); /** * Initiates an object of type UrlDefinitionsToMessages. * * @param classes the classes with url-infos. */ public UrlDefinitionsToMessages(Class<?>... classes) { classesWithUrlInfos = classes; fillFormatDefinitions(classesWithUrlInfos); } /** * Add all URL constants to a {@link Properties} with prefix 'url'. * * @deprecated Use {@link #addUrlsAsMessagesWithPositionedParameters()} * instead, have in mind that url -> purl * */ @Deprecated public void addUrlsAsMessages() { addConstantInfosFromClass("url", classesWithUrlInfos); } /** * Add all URL constants to a {@link Properties} with prefix 'purl'. * */ public void addUrlsAsMessagesWithPositionedParameters() { addConstantInfosFromClass("purl", classesWithUrlInfos); } /** * Add all URL constants to a {@link Properties} with prefix 'nurl'. * */ public void addUrlsAsMessagesWithNamedParameters() { addConstantInfosFromClass("nurl", classesWithUrlInfos); } /** * Add all Params constants to a {@link Properties}. * */ public void addParamsAsMessages() { addConstantInfosFromClass(MESSAGE_SOURCE_PRAEFIX_PARAMETER, classesWithUrlInfos); } /** * Add all Paramgroup constants to a {@link Properties} this means something * like "user_id = {1}, user_name={2}". This can be used in @{url()} in * Thymeleaf. * */ public void addParamGroupAsMessages() { addConstantInfosFromClass(MESSAGE_SOURCE_PRAEFIX_PARAMETER_GROUP, classesWithUrlInfos); } /** * @return the messages */ public Properties getMessages() { return messages; } /** * @return the formatDefinition */ Map<String, String> getFormatDefinition() { return formatDefinition; } /** * Added the information of formatting parametes to the given map. * * @param classes classes which declares the parmaters. */ private void fillFormatDefinitions(Class<?>[] classes) { for (Class<?> class1 : classes) { if (Modifier.isInterface(class1.getModifiers())) { Field[] fields = class1.getDeclaredFields(); for (Field field : fields) { try { if (Modifier.isPublic(field.getModifiers()) && field.getName() .startsWith(PRAEFIX_PARAMETER)) { final ParamFormat format = field.getAnnotation(ParamFormat.class); final String paramName = field.get(null).toString(); if (formatDefinition.containsKey(paramName)) { LOG.warn("Parameter {} is defined more " + "than once.", paramName); } else { formatDefinition.put(paramName, ""); } if (format != null) { formatDefinition.put(paramName, format.value()); } } } catch (IllegalArgumentException | IllegalAccessException e) { LOG.error( "Error reading the field " + field.getDeclaringClass() + "." + field.getName(), e); } } fillFormatDefinitions(class1.getDeclaredClasses()); } } } /** * Add all class Constants to a {@link Properties}. * * @param prefix the prefix for the key. * @param classes the list of classes. */ private void addConstantInfosFromClass(String prefix, Class<?>[] classes) { for (Class<?> class1 : classes) { if (Modifier.isInterface(class1.getModifiers())) { Field[] fields = class1.getDeclaredFields(); for (Field field : fields) { if (Modifier.isPublic(field.getModifiers())) { addFieldInformation(prefix, formatDefinition, field); } } addConstantInfosFromClass( prefix + "." + class1.getSimpleName(), class1.getDeclaredClasses()); } } } /** * Analyse the field and add the information to the properties. * * @param prefix the prefix each key should get. * @param formatDefinition the definition for the format of parameters. * @param field the field which contains information. */ private void addFieldInformation(String prefix, Map<String, String> formatDefinition, Field field) { try { if (prefix.startsWith("url") || prefix.startsWith("purl")) { addURLsWithPositionedParameters(prefix, formatDefinition, field); } else if (prefix.startsWith("nurl")) { addUrlsWithNamedParameters(prefix, formatDefinition, field); } else if (prefix.startsWith(MESSAGE_SOURCE_PRAEFIX_PARAMETER)) { addSimpleParameters(prefix, field); } else if (prefix .startsWith(MESSAGE_SOURCE_PRAEFIX_PARAMETER_GROUP)) { addParameterGroups(prefix, formatDefinition, field); } else { throw new IllegalArgumentException("Invalid Prefix " + prefix); } } catch (IllegalArgumentException | IllegalAccessException e) { LOG.error("Error reading the field " + field.getDeclaringClass() + "." + field.getName(), e); } } /** * Add alls URLs and prepare paramters, so that the will replaced by * position. * * @param prefix the prefix of the key. * @param formatDefinition information about the format defintion. * @param field the field whith the url. * @throws IllegalAccessException */ private void addURLsWithPositionedParameters(String prefix, Map<String, String> formatDefinition, Field field) throws IllegalAccessException { if (!field.getName().startsWith(PRAEFIX_PARAMETER) && !field.getName().startsWith(PRAEFIX_PARAMETER_GROUP)) { final String keyName = createKey(prefix, field.getDeclaringClass().getSimpleName(), field.getName()); final String urlValue = createUrl(URLCleaner.removeRegexpFromUrl(field.get(null) .toString()), formatDefinition); messages.put(keyName, urlValue); } } /** * Add alls URLs and prepare paramters, so that the will replaced by name. * * @param prefix the prefix of the key. * @param formatDefinition information about the format defintion. * @param field the field whith the url. * @throws IllegalAccessException */ private void addUrlsWithNamedParameters(String prefix, Map<String, String> formatDefinition, Field field) throws IllegalAccessException { if (!field.getName().startsWith(PRAEFIX_PARAMETER) && !field.getName().startsWith(PRAEFIX_PARAMETER_GROUP)) { final String keyName = createKey(prefix, field.getDeclaringClass().getSimpleName(), field.getName()); final String urlValue = createUrlWithNamedParams( URLCleaner.removeRegexpFromUrl(field.get(null) .toString()), formatDefinition); messages.put(keyName, urlValue); } } /** * Add simple parameter names. * * @param prefix the prefix of the key. * @param field the field whith the url. * @throws IllegalAccessException */ private void addSimpleParameters(String prefix, Field field) throws IllegalAccessException { if (field.getName().startsWith(PRAEFIX_PARAMETER)) { final String keyName = createKey(prefix, field.getDeclaringClass().getSimpleName(), field .getName().substring(2)); messages.put(keyName, field.get(null).toString()); } } /** * Add group of parameter names. * * @param prefix the prefix of the key. * @param field the field whith the url. * @throws IllegalAccessException */ private void addParameterGroups(String prefix, Map<String, String> formatDefinition, Field field) throws IllegalAccessException { if (field.getName().startsWith(PRAEFIX_PARAMETER_GROUP)) { final String keyName = createKey(prefix, field.getDeclaringClass().getSimpleName(), field .getName().substring(3)); final String pgValue = createParamGroup(field.get(null).toString(), formatDefinition); messages.put(keyName, pgValue); } } /** * Creates the URL from the constant as a message, i.e. named parameters * like {user_id} will be replaced by ${user_id}. * * @param urlAsString the url. * @param formatDefinition the format definitions. * @return the URL as parameterized message. */ private String createUrlWithNamedParams(String urlAsString, Map<String, String> formatDefinition) { final StringBuilder result = new StringBuilder(urlAsString.length()); final StringTokenizer tokens = new StringTokenizer(urlAsString, "{}"); boolean isVariable = (urlAsString.charAt(0) == '{'); while (tokens.hasMoreTokens()) { final String key = tokens.nextToken(); if (isVariable) { String varName = key; String format = formatDefinition.get(varName); if (format == null) { LOG.warn("In URL {} you use an undefined parameter {}", urlAsString, varName); format = ""; } result.append("$'{'").append(varName).append("'}'"); } else { result.append(key); } isVariable = !isVariable; } return result.toString(); } /** * Creates the URL from the constant as a message, i.e. named parameters * like {user_id} will be replaced by {0}. * * @param urlAsString the url. * @param formatDefinition the format definitions. * @return the URL as parameterized message. */ private String createUrl(String urlAsString, Map<String, String> formatDefinition) { final StringBuilder result = new StringBuilder(urlAsString.length()); final StringTokenizer tokens = new StringTokenizer(urlAsString, "{}"); boolean isVariable = (urlAsString.charAt(0) == '{'); int i = 0; while (tokens.hasMoreTokens()) { final String key = tokens.nextToken(); if (isVariable) { String format = formatDefinition.get(key); if (format == null) { LOG.warn("In URL {} you use an undefined parameter {}", urlAsString, key); format = ""; } result.append('{').append(i).append(format).append('}'); i++; } else { result.append(key); } isVariable = !isVariable; } return result.toString(); } /** * Creates the Paramgroup from the constant as a message, i.e. paramaters * will be enriched with ={} * * @param definitionOfParams the field value. * @param formatDefinition the format definition. * @return the paramgroup as parameterized message. */ private String createParamGroup(String definitionOfParams, Map<String, String> formatDefinition) { final StringBuilder result = new StringBuilder(2 * definitionOfParams.length()); final StringTokenizer tokens = new StringTokenizer(definitionOfParams, ","); int i = 0; while (tokens.hasMoreTokens()) { final String key = tokens.nextToken().trim(); String format = formatDefinition.get(key); if (format == null) { LOG.warn("In Paramgroup {} you use an undefined parameter {}", definitionOfParams, key); format = ""; } result.append(key).append("={").append(i).append(format) .append("},"); i++; } return result.substring(0, result.length() - 1); } /** * Create a key name. * * @param prefix the prefix for the key. * @param className the simple name of the declaring class. * @param fieldName the field name. * @return a message-key. */ private String createKey(String prefix, String className, String fieldName) { return (prefix + "." + className + "." + fieldName).toLowerCase(); } }