net.yadaframework.web.dialect.YadaDialectUtil Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of yadaweb Show documentation
Show all versions of yadaweb Show documentation
Some useful tasks for the Yada Framework
package net.yadaframework.web.dialect;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thymeleaf.IEngineConfiguration;
import org.thymeleaf.context.ITemplateContext;
import org.thymeleaf.model.ICloseElementTag;
import org.thymeleaf.model.IModel;
import org.thymeleaf.model.IOpenElementTag;
import org.thymeleaf.model.ITemplateEvent;
import org.thymeleaf.standard.expression.IStandardExpression;
import org.thymeleaf.standard.expression.IStandardExpressionParser;
import org.thymeleaf.standard.expression.StandardExpressions;
import net.yadaframework.components.YadaUtil;
import net.yadaframework.core.YadaConfiguration;
import net.yadaframework.exceptions.YadaInvalidUsageException;
public class YadaDialectUtil {
private final transient Logger log = LoggerFactory.getLogger(getClass());
private final YadaConfiguration config;
static final String YADA_PREFIX = "yada";
static final String THYMELEAF_PREFIX = "th";
public final static String YADA_PREFIX_WITHCOLUMN = YADA_PREFIX + ":";
public final static String THYMELEAF_PREFIX_WITHCOLUMN = THYMELEAF_PREFIX + ":";
private enum AppendType {
NONE,
APPEND,
PREPEND,
APPEND_WITH_SPACE,
PREPEND_WITH_SPACE;
}
public YadaDialectUtil(YadaConfiguration config) {
this.config=config;
}
/**
* Returns the HTML contained in a tag, as a string
* @param model
* @param openNodeIndex the index of the open tag in the model
* @return
*/
public String getInnerHtml(IModel model, int openNodeIndex) {
StringBuilder result = new StringBuilder();
ITemplateEvent iTemplateEvent = model.get(openNodeIndex);
if (!(iTemplateEvent instanceof IOpenElementTag)) {
throw new YadaInvalidUsageException("The openNodeIndex should point to an open tag");
}
final IOpenElementTag openTag = (IOpenElementTag) iTemplateEvent;
String outerTagName = openTag.getElementCompleteName();
int i = openNodeIndex;
while (++i T parseExpression(String value, ITemplateContext context, Class resultClass) {
try {
final IEngineConfiguration configuration = context.getConfiguration();
final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);
final IStandardExpression expression = parser.parseExpression(context, value);
return (T) expression.execute(context);
} catch (RuntimeException e) {
log.trace("Expression evaluation of \"{}\" failed - using as literal string", value);
if (resultClass.equals(String.class)) {
return (T) value; // Maybe it's just a string
}
throw e;
}
}
/**
* Concatenate some strings using the given joiner, checking that the joiner is not added when already present and it is trimmed
* from the result.
* Example:
* joinStrings(" - ", "a, b", "c, d") = "a, b - c, d"
* joinStrings(" - ", "a, b - ", "c, d") = "a, b - c, d"
* joinStrings(" - ", "a, b", "c, d - ") = "a, b - c, d"
* @param joiner a string to use as a joiner (separator)
* @param start the initial string
* @param append a number of other strings
* @return a string where all parameters have been joined with the joiner string that appears only once and not at the result edges
*/
public String joinStrings(String joiner, String start, String ... append) {
StringBuilder result = new StringBuilder(StringUtils.removeEnd(start, joiner));
result.append(joiner);
for (String a : append) {
result.append(StringUtils.removeEnd(StringUtils.removeStart(a, joiner), joiner));
result.append(joiner);
}
return StringUtils.removeEnd(result.toString(), joiner);
}
/**
* Returns a unique identifier on the page, for the given tag
* @param someTag
* @return
*/
public String makeYadaTagId(ITemplateEvent someTag) {
return someTag.getLine() + "c" + someTag.getCol() + "r" + YadaUtil.INSTANCE.getRandom(0, Integer.MAX_VALUE);
}
/**
* Retrieves a map of attributes from the custom tag, where all HTML attributes are kept as they are (NO: and thymeleaf
* th: attributes are converted to HTML attributes when possible). The map is then converted to a comma-separated string
* @param customTag
* @param context
* @return a comma-separated string of name=value attributes to be used in th:attr
*/
public String getConvertedCustomTagAttributeString(IOpenElementTag customTag, ITemplateContext context, String...ignoreAttributes) {
// First, get all HTML attributes
Set ignore = new HashSet();
for (int i=0; i newAttributes = getHtmlAttributes(customTag, ignore);
// NO: Then convert all th: attributes to HTML attributes
// This was used when the YadaDialect precedence was the same as the StandardDialect
// convertThAttributes(customTag, newAttributes, context);
return YadaUtil.INSTANCE.mapToString(newAttributes);
}
/**
* Get all HTML attributes from the custom sourceTag
* @param sourceTag
* @return the HTML attributes found on the tag
*/
private Map getHtmlAttributes(IOpenElementTag sourceTag, Set ignore) {
Map newAttributes = new HashMap<>();
Map sourceAttributes = sourceTag.getAttributeMap();
for (Map.Entry sourceAttribute : sourceAttributes.entrySet()) {
String attributeName = sourceAttribute.getKey().toLowerCase();
String attributeValue = sourceAttribute.getValue();
if (!attributeName.startsWith(YADA_PREFIX_WITHCOLUMN) &&
!attributeName.startsWith(THYMELEAF_PREFIX_WITHCOLUMN) &&
!ignore.contains(attributeName)) {
if ("type".equalsIgnoreCase(attributeName) && "number".equalsIgnoreCase(attributeValue)) {
// The "type='number'" attribute must be removed from the output tag because it is handled in a custom way
continue;
}
// Convert null to name
if (attributeValue==null) {
attributeValue = attributeName;
}
// Escape single quote
attributeValue = attributeValue.replaceAll("'", "\\\\'");
// Add single quote around value so that equal sign doesn't mess up th:attr
attributeValue = "'" + attributeValue + "'";
newAttributes.put(attributeName, attributeValue);
// // Skip attributes with empty value (didn't find a way to set an empty attribute with thymeleaf!)
// if (attributeValue.length()>0) {
// }
}
}
return newAttributes;
}
@Deprecated // Not used anymore because the YadaDialect has a higher precedence so th attributes are stripped before
private void convertThAttributes(IOpenElementTag sourceTag, Map newAttributes, ITemplateContext context) {
Map sourceAttributes = sourceTag.getAttributeMap();
final IEngineConfiguration configuration = context.getConfiguration();
final IStandardExpressionParser parser = StandardExpressions.getExpressionParser(configuration);
// Most of th: attributes have a direct equivalent, some must have a special treatment
for (Map.Entry sourceAttribute : sourceAttributes.entrySet()) {
String fullThAttributeName = sourceAttribute.getKey();
String attributeValue = sourceAttribute.getValue();
if (fullThAttributeName.startsWith(THYMELEAF_PREFIX_WITHCOLUMN)) {
String attributeName = removePrefix(fullThAttributeName, THYMELEAF_PREFIX); // from "th:value" to "value"
String parsedValue = null;
if (!"attr".equals(attributeName) && !"attrappend".equals(attributeName) && !"attrprepend".equals(attributeName)) {
// attr and similar attributes don't use expressions as value
try {
final IStandardExpression expression = parser.parseExpression(context, attributeValue);
parsedValue = (String) expression.execute(context);
} catch (Exception e) {
log.debug("Can't parse \"{}\" for attribute \"{}\" - skipping", attributeValue, fullThAttributeName);
continue; // Next attribute
}
}
// Hanlde some th attributes
switch (attributeName) {
case "field":
// Add "name" and "value" attributes
newAttributes.put("name", parsedValue);
newAttributes.put("value", parsedValue);
break;
case "attr":
handleAttr(attributeValue, newAttributes, AppendType.NONE, context, parser);
break;
case "alt-title":
// Add "alt" and "title" attributes
newAttributes.put("alt", parsedValue);
newAttributes.put("title", parsedValue);
break;
case "lang-xmllang":
// Add "lang" and "xmllang" attributes
newAttributes.put("lang", parsedValue);
newAttributes.put("xmllang", parsedValue);
break;
case "attrappend":
handleAttr(attributeValue, newAttributes, AppendType.APPEND, context, parser);
break;
case "attrprepend":
handleAttr(attributeValue, newAttributes, AppendType.PREPEND, context, parser);
break;
case "classappend":
appendMapValue("class", attributeValue, AppendType.APPEND_WITH_SPACE, newAttributes);
break;
case "styleappend":
appendMapValue("style", attributeValue, AppendType.APPEND, newAttributes);
break;
default:
// Most th: attributes have plain HTML equivalent
// but as they must be inserted via th:attr in the template, I need to convert the value
// into a sum of strings in order to handle single quotes correctly.
// Example original html:
// Example of final html:
parsedValue = parsedValue.replaceAll("'", "'+'\\\\''+'");
// The parsedValue is now a plain string so we quote it to be EL compatible
newAttributes.put(attributeName, "'" + parsedValue + "'");
}
}
}
}
/**
* Handles "th:attr", "th:attrappend", "th:attrprepend" by splitting the comma-separated string into individual name-values pairs for newAttributes
* @param attributeValue
* @param newAttributes
* @param appendType
* @param context
* @param parser
*/
private void handleAttr(String attributeValue, Map newAttributes, AppendType appendType, ITemplateContext context, IStandardExpressionParser parser) {
// Add each comma-separated attribute
String[] attrAttributes = attributeValue.split(" *, *");
for (String attrAttribute : attrAttributes) {
// "name=value"
String[] nameThenValue = attrAttribute.split(" *= *");
String name = nameThenValue[0];
String value = nameThenValue[1];
final IStandardExpression attrExpression = parser.parseExpression(context, value);
String finalValue = (String) attrExpression.execute(context);
appendMapValue(name, finalValue, appendType, newAttributes);
}
}
/**
* The value is appended to existing values found in the map with the same name
* @param name
* @param value
* @param appendType
* @param newAttributes
*/
private void appendMapValue(String name, String value, AppendType appendType, Map newAttributes) {
String current = newAttributes.get(name);
if (StringUtils.isNotBlank(current)) {
if (appendType==AppendType.APPEND) {
// Append to current value
value = current + value;
} else if (appendType==AppendType.PREPEND) {
// Prepend to current value
value = value + current;
} else if (appendType==AppendType.APPEND_WITH_SPACE) {
// Prepend to current value
value = current + " " + value;
} else if (appendType==AppendType.PREPEND_WITH_SPACE) {
// Prepend to current value
value = value + " " + current;
}
}
newAttributes.put(name, value);
}
/**
* Remove the dialect prefix from the start of the value
* @param value e.g. "yada:someAttributeName"
* @param dialectPrefixNoColumn e.g. "yada"
* @return the value stripped from the starting dialect prefix, e.g. "someAttributeName"
*/
public String removePrefix(String value, String dialectPrefixNoColumn) {
return StringUtils.removeStart(value, dialectPrefixNoColumn + ":");
}
/**
* Browser cache bypass trick for resources.
* Converts "/res/xxx" into "/res-123/xxx", "/yadares/xxx" into "/yadares-7/xxx", where the number is the application build number.
*/
public String getVersionedAttributeValue(String value) {
try {
if (StringUtils.isBlank(value)) {
return value;
}
if (value.startsWith("//")) {
return value;
}
if (!value.startsWith("/")) {
return value; // Don't handle relative paths, for speed
}
// The contextPath is applied by @{} so it's not needed here
// String contextPath = ((org.thymeleaf.context.IWebContext)context).getRequest().getContextPath();
int dividerPos = value.indexOf('/', 1); // Second slash
if (dividerPos<0) {
return value; // No second slash
}
String valueType = value.substring(1, dividerPos); // e.g. "res"
String valueSuffix = value.substring(dividerPos); // e.g. "/xxx"
boolean isResource = config.getResourceDir().equals(valueType);
if (isResource) {
return applyVersion(config.getVersionedResourceDir(), valueSuffix); // /site/res-0002/xxx
}
boolean isYada = config.getYadaResourceDir().equals(valueType);
if (isYada) {
return applyVersion(config.getVersionedYadaResourceDir(), valueSuffix); // /site/yadares-7/xxx
}
// The "contents" folder is not versioned anymore.
// Cache bypass is better implemented by versioning the file name of anything stored there.
// YadaAttachedFile should do this automatically.
// The problem with contents is that the version should be taken from the file timestamp so here it should accept any value but I don't know how to make it work with any version value
// boolean isContent = config.getContentName().equals(valueType);
// if (isContent) {
// String contentUrlBase = config.getContentUrl(); // e.g. "/contents" or "http://somecdn.com/somecontext"
// if (config.isContentUrlLocal()) {
// return applyVersion(contentUrlBase.substring(1) + "/" + config.getApplicationBuild(), valueSuffix); // /site/contents/002/xxx
// }
// return contentUrlBase + "/" + valueSuffix; // e.g. http://somecdn.com/somecontext/xxx
// }
} catch (Exception e) {
log.error("getVersionedAttributeValue failed for value='{}'", value, e);
}
return value;
}
/**
*
* @param versionedDir without leading /
* @param valueSuffix
* @return
*/
private String applyVersion(String versionedDir, String valueSuffix) {
StringBuilder result = new StringBuilder("/").append(versionedDir).append(valueSuffix); // e.g. "/res-0002/xxx
return result.toString();
}
}