org.zkoss.zk.ui.metainfo.impl.AnnotationHelper Maven / Gradle / Ivy
/* AnnotationHelper.java
Purpose:
Description:
History:
Mon Aug 6 15:48:07 2007, Created by tomyeh
Copyright (C) 2007 Potix Corporation. All Rights Reserved.
{{IS_RIGHT
This program is distributed under LGPL Version 2.1 in the hope that
it will be useful, but WITHOUT ANY WARRANTY.
}}IS_RIGHT
*/
package org.zkoss.zk.ui.metainfo.impl;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import org.zkoss.lang.Strings;
import org.zkoss.util.Maps;
import org.zkoss.util.resource.Location;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.metainfo.AnnotationMap;
import org.zkoss.zk.ui.metainfo.ComponentInfo;
import org.zkoss.zk.ui.metainfo.ShadowInfo;
import org.zkoss.zk.ui.sys.ComponentCtrl;
/**
* A helper class used to parse annotations.
*
* How to use:
*
* - Invoke {@link #add}
* or {@link #addByCompoundValue} to add annotations to this helper.
* - After annotations are all added, invoke {@link #applyAnnotations}
* to update the annotations to the specified component info.
*
*
* @author tomyeh
* @since 3.0.0
*/
public class AnnotationHelper {
/** A list of AnnotInfo */
final List _annots = new LinkedList();
private boolean _ignoreAnnotNamespace;
/** Test if the given value is an annotation.
* In other words, it returns true if the value matches
* one of two kinds of format described in {@link #addByCompoundValue}.
* @param val the value.
* @since 6.0.0
*/
public static boolean isAnnotation(String val) {
int len = val.length();
if (len >= 4) {
len = (val = val.trim()).length();
if (len >= 4 && val.charAt(0) == '@') {
if (val.charAt(1) == '{') {
if (val.charAt(len - 1) == '}') //format 1
return true;
} else if (val.charAt(len - 1) == ')') {
//we have to be conservative since a non-annotation value might carry @
int j = Strings.skipWhitespaces(val, 1);
char cc = val.charAt(j);
//annotation must start with the above characters
if ((cc >= 'a' && cc <= 'z') || (cc >= 'A' && cc <= 'Z') || cc == '_' || cc == '$') {
for (; j < len; ++j) {
switch (cc = val.charAt(j)) {
case '(':
return true; //valid
case '_':
case '$':
case '.':
case '-':
continue; //valid
default:
if (Character.isWhitespace(cc)) {
j = Strings.skipWhitespaces(val, j + 1);
if (j < len && val.charAt(j) == '(')
return true;
return false;
}
if ((cc < 'a' || cc > 'z') && (cc < 'A' || cc > 'Z') && (cc < '0' || cc > '9'))
return false;
}
}
}
}
}
}
return false;
}
/** Adds an annotation definition.
* The annotation's attributes must be parsed into a map (annotAttrs).
*
* @param annotName the annotation name.
* @param annotAttrs a map of attributes of the annotation. If null,
* it means no attribute at all.
* @param loc the location information of the annotation in
* the document, or null if not available.
* @see #addByCompoundValue
*/
public void add(String annotName, Map annotAttrs, Location loc) {
if (annotName == null || annotName.length() == 0)
throw new IllegalArgumentException("empty");
_annots.add(new AnnotInfo(annotName, annotAttrs, loc));
}
/** @deprecated As of release 6.0.1, replaced with {@link #add(String, Map, Location)}.
*/
public void add(String annotName, Map annotAttrs) {
add(annotName, annotAttrs, null);
}
/** Adds annotation by specifying the content in the compound format.
* There are two formats:
*
Format 1 (recommended, since 6.0):
* @annot-name(att1-name=att1-value, att2-name=att2-value) @annot-name() @default(annot3-attrs)
*
Format 2 (deprecated, 5.0 and before):
* @{annot-name(att1-name=att1-value, att2-name=att2-value) annot2-name (annot3-attrs)}
*
In the first format, it must be a list of annotations separated by space.
* And, each annotation is in the format of @annot-name(key=value, value, key=value)
.
* That is, it starts with the annotation's name and a parenthesis to enclose
* any number of key and value pairs (key is optional).
* The annotation's names must be composed of letters, numbers, the underscore _
, the dash -
and the dollar sign $
.
* The names may only begin with a letter, the underscore or a dollar sign.
* In additions, all characters are preserved, including the single and double quotes.
*
* @param cval the compound value to check. This method assumes that
* cval starts with @ and the length is larger than 2
* @param loc the location information of the value for displaying better
* error message. Ignored if null.
* @since 6.0.0
*/
public void addByCompoundValue(String cval, Location loc) {
final int len = cval.length();
if (cval.charAt(1) == '{' && cval.charAt(len - 1) == '}') { //Format 1
addInV5(cval.substring(2, len - 1));
return;
}
//format 2
//for each @name(value),
//parse name/value, then pass to addByRawValueInV6
for (int j = 0; j >= 0; j = cval.indexOf('@', j)) {
//look for annotation's name
int k = cval.indexOf('(', ++j);
if (k < 0)
throw wrongAnnotationException(cval, "'(' expected", loc);
final String annotName = cval.substring(j, k).trim();
j = ++k;
final StringBuffer sb = new StringBuffer(len);
int nparen = 1;
for (char quot = (char) 0;; ++j) {
if (j >= len)
throw wrongAnnotationException(cval, "')' expected", loc);
char cc = cval.charAt(j);
if (quot == (char) 0) {
if (cc == '(') {
++nparen;
} else if (cc == ')' && --nparen == 0) { //found
addByRawValueInV6(annotName, sb.toString().trim(), loc);
break; //next @name(value)
} else if (cc == '\'' || cc == '"') {
quot = cc; //begin-of-quote
}
} else if (cc == quot) {
quot = (char) 0; //end-of-quote
}
sb.append(cc);
if (cc == '\\' && j < len - 1)
sb.append(cval.charAt(++j));
//Note: we don't decode \x. Rather, we preserve it such
//that the data binder can use them
}
}
}
/** @param rval att1-name=att1-value, att2-name = att2-value
*/
private void addByRawValueInV6(String annotName, String rval, Location loc) {
final Map attrs = new LinkedHashMap(4);
final int len = rval.length();
final StringBuffer sb = new StringBuffer(len);
String nm = null;
char quot = (char) 0;
int nparen = 0;
main: //for each name=value, parse name and value
for (int j = 0;; ++j) {
if (j >= len) {
if (quot != (char) 0)
throw wrongAnnotationException(rval, quot + " expected (not paired)", loc);
if (nparen != 0)
throw wrongAnnotationException(rval, "')' expected", loc);
final String val = sb.toString().trim();
if (nm != null || val.length() > 0) //skip empty one (including after last , )
attrs.put(nm, new String[] { val }); //found
break; //done
}
char cc = rval.charAt(j);
if (quot == (char) 0) {
if (cc == ',' && nparen == 0) {
final String val = sb.toString().trim();
if (nm == null && val.length() == 0)
throw wrongAnnotationException(rval, "nothing before ','", loc);
attrs.put(nm, new String[] { val }); //found
nm = null; //cleanup
sb.setLength(0); //cleanup
continue; //next name=value
} else if (cc == '=' && nparen == 0) {
if (nm != null)
throw wrongAnnotationException(rval, "',' missed between two equal sign (=)", loc);
nm = sb.toString().trim(); //name found
sb.setLength(0); //cleanup
continue; //parse value
} else if (cc == '(') {
++nparen;
} else if (cc == ')') {
if (--nparen < 0)
throw wrongAnnotationException(rval, "too many ')'", loc);
} else if (cc == '\'' || cc == '"') {
quot = cc;
} else if (cc == '{' && nparen == 0 && (sb.length() == 0 || sb.toString().trim().length() == 0)) {
//look for }
for (int k = ++j, ncur = 1;; ++j) {
if (j >= len)
throw wrongAnnotationException(rval, "'}' expected", loc);
cc = rval.charAt(j);
if (quot == (char) 0) {
if (cc == '}' && --ncur == 0) { //found
attrs.put(nm, parseValueArray(rval.substring(k, j).trim(), loc));
j = Strings.skipWhitespaces(rval, j + 1);
if (j < len && rval.charAt(j) != ',')
throw wrongAnnotationException(rval, "',' expected, not '" + rval.charAt(j) + '\'',
loc);
nm = null; //cleanup
sb.setLength(0); //cleanup
continue main;
} else if (cc == '{') {
++ncur;
} else if (cc == '\'' || cc == '"') {
quot = cc;
}
} else if (cc == quot) {
quot = (char) 0;
}
if (cc == '\\' && j < len - 1)
++j; //skip next \
}
}
} else if (cc == quot) {
quot = (char) 0;
}
sb.append(cc);
if (cc == '\\' && j < len - 1)
sb.append(rval.charAt(++j));
//Note: we don't decode \x. Rather, we preserve it such
//that the data binder can use them
}
//TODO pass loc only in some condition, e.g. debug or non-production
add(annotName, attrs, loc);
}
/** Parses the attribute value.
* If the value starts with { and ends with }, an array of String is returned.
* Otherwise, the value is returned directly (without any processing).
* @param val the value. This method assumes val has been trimmed before the
* call.
* @exception NullPointerException if val is null.
* @param loc the location information of the value for displaying better
* error message. Ignored if null.
* @since 6.0.0
*/
public static String[] parseAttributeValue(String val, Location loc) {
final int len = val.length();
if (len >= 2 && val.charAt(0) == '{' && val.charAt(len - 1) == '}')
return parseValueArray(val.substring(1, len - 1), loc);
return new String[] { val };
}
private static String[] parseValueArray(String rval, Location loc) {
final List attrs = new ArrayList();
final int len = rval.length();
char quot = (char) 0;
final StringBuffer sb = new StringBuffer(len);
int nparen = 0;
for (int j = 0;; ++j) {
if (j >= len) {
if (quot != (char) 0)
throw wrongAnnotationException(rval, '\'' + quot + "' expected (not paired)", loc);
if (nparen != 0)
throw wrongAnnotationException(rval, "')' expected", loc);
final String val = sb.toString().trim();
if (val.length() > 0) //skip if last if it is empty
attrs.add(val);
break; //done
}
char cc = rval.charAt(j);
if (quot == (char) 0) {
if (cc == ',' && nparen == 0) { //found
attrs.add(sb.toString().trim()); //including empty (between ,)
sb.setLength(0); //cleanup
continue;
} else if (cc == '(') {
++nparen;
} else if (cc == ')') {
if (--nparen < 0)
throw wrongAnnotationException(rval, "too many ')'", loc);
} else if (cc == '\'' || cc == '"') {
quot = cc;
}
} else if (cc == quot) {
quot = (char) 0;
}
sb.append(cc);
if (cc == '\\' && j < len - 1)
sb.append(rval.charAt(++j));
//Note: we don't decode \x. Rather, we preserve it such
//that the data binder can use them
}
return attrs.toArray(new String[attrs.size()]);
}
private static UiException wrongAnnotationException(String cval, String reason, Location loc) {
final String msg = "Illegal annotation, " + reason + ": " + cval;
return new UiException(loc != null ? loc.format(msg) : msg);
}
private void addInV5(String cval) {
final char[] seps1 = { '(', ' ' }, seps2 = { ')' };
for (int j = 0, len = cval.length(); j < len;) {
j = Strings.skipWhitespaces(cval, j);
int k = Strings.nextSeparator(cval, j, seps1, true, true, false);
if (k < len && cval.charAt(k) == '(') {
String nm = cval.substring(j, k).trim();
if (nm.length() == 0)
nm = "default";
j = k + 1;
k = Strings.nextSeparator(cval, j, seps2, true, true, false);
final String rv = (k < len ? cval.substring(j, k) : cval.substring(j)).trim();
if (rv.length() > 0)
addByRawValueInV5(nm, rv);
else
add(nm, null);
} else {
final String rv = (k < len ? cval.substring(j, k) : cval.substring(j)).trim();
if (rv.length() > 0)
addByRawValueInV5("default", rv);
}
j = k + 1;
}
}
/** @param rval att1-name=att1-value, att2-name = att2-value
*/
@SuppressWarnings("unchecked")
private void addByRawValueInV5(String annotName, String rval) {
//The parsing of the value in format 1 is different from format 2
final Map attrs = (Map) Maps.parse(null, rval, ',', '\'', true);
for (Map.Entry me : attrs.entrySet())
me.setValue(new String[] { (String) me.getValue() });
//convert String to String[]
add(annotName, (Map) attrs);
}
/** Applies the annotations defined in this helper to the specified
* instance definition.
*
* @param compInfo the instance definition to update
* @param propName the property name
* @param clear whether to clear all definitions before returning
* @see #clear
* @since 6.0.1
*/
public void applyAnnotations(ComponentInfo compInfo, String propName, boolean clear) {
for (AnnotInfo info : _annots) {
compInfo.addAnnotation(propName, info.name, info.attrs, info.loc);
}
if (clear)
_annots.clear();
}
/** Applies the annotations defined in this helper to the specified
* instance definition.
*
* @param compInfo the instance definition to update
* @param propName the property name
* @param clear whether to clear all definitions before returning
* @see #clear
* @since 8.0.0
*/
public void applyAnnotations(ShadowInfo compInfo, String propName, boolean clear) {
for (AnnotInfo info : _annots) {
compInfo.addAnnotation(propName, info.name, info.attrs, info.loc);
}
if (clear)
_annots.clear();
}
/** @deprecated As of release 6.0.1, replaced
* with {@link #applyAnnotations(ComponentInfo,String,boolean)}.
*/
public void applyAnnotations(ComponentInfo compInfo, String propName, boolean clear, Location loc) {
applyAnnotations(compInfo, propName, clear);
}
/** Applies the annotations defined in this helper to the specified
* component.
*
* @param comp the component to update
* @param propName the property name
* @param clear whether to clear all definitions before returning
* @see #clear
*/
public void applyAnnotations(Component comp, String propName, boolean clear) {
for (AnnotInfo info : _annots) {
ComponentCtrl ctrl = (ComponentCtrl) comp;
ctrl.addAnnotation(propName, info.name, info.attrs);
}
if (clear)
_annots.clear();
}
/** Applies the annotations defined in this helper to the specified
* annotation map.
*
* @param annots the annotation map where the annotations are added.
* @param propName the property name
* @param clear whether to clear all definitions before returning
* @see #clear
* @since 6.0.1
*/
public void applyAnnotations(AnnotationMap annots, String propName, boolean clear) {
for (AnnotInfo info : _annots)
annots.addAnnotation(propName, info.name, info.attrs, info.loc);
if (clear)
_annots.clear();
}
/** Clears the annotations defined in this helper.
*
* The annotations are defined by {@link #add}
* or {@link #addByCompoundValue}.
*
* @return true if one or more annotation definitions are defined
* (thru {@link #add}).
*/
public boolean clear() {
if (!_annots.isEmpty()) {
_annots.clear();
return true;
}
return false;
}
/**
* Whether to ignore annotation namespace.
* @return true if should ignore annotation namespace.
* @since 8.5.2
*/
public boolean shouldIgnoreAnnotNamespace() {
return _ignoreAnnotNamespace;
}
/**
* Sets whether to ignore annotation namespace.
* @param ignoreAnnotNamespace whether to ignore annotation namespace
* @since 8.5.2
*/
public void setIgnoreAnnotNamespace(boolean ignoreAnnotNamespace) {
_ignoreAnnotNamespace = ignoreAnnotNamespace;
}
private static class AnnotInfo {
private final String name;
private final Map attrs;
private final Location loc;
private AnnotInfo(String name, Map attrs, Location loc) {
this.name = name;
this.attrs = attrs;
this.loc = loc;
}
}
}