zk/src/main/java/org/zkoss/zk/ui/metainfo/impl/AnnotationHelper.java

Summary

Maintainability
F
6 days
Test Coverage
/* 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.
 *
 * <p>How to use:
 * <ol>
 * <li>Invoke {@link #add}
 * or {@link #addByCompoundValue} to add annotations to this helper.</li>
 * <li>After annotations are all added, invoke {@link #applyAnnotations}
 * to update the annotations to the specified component info.</li>
 * </ol>
 *
 * @author tomyeh
 * @since 3.0.0
 */
public class AnnotationHelper {
    /**
     * A list of AnnotInfo
     */
    final List<AnnotInfo> _annots = new LinkedList<AnnotInfo>();

    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<String, String[]> annotAttrs,
            Location loc) {
        if (annotName == null || annotName.length() == 0)
            throw new IllegalArgumentException("empty");
        _annots.add(new AnnotInfo(annotName, annotAttrs, loc));
    }

    /**
     * Adds annotation by specifying the content in the compound format.
     * <p>There are two formats:
     * <p>Format 1 (recommended, since 6.0):<br/>
     * <code>@annot-name(att1-name=att1-value, att2-name=att2-value) @annot-name() @default(annot3-attrs)</code>
     * <p>In the first format, it must be a list of annotations separated by space.
     * And, each annotation is in the format of <code>@annot-name(key=value, value, key=value)</code>.
     * 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 <code>_</code>, the dash <code>-</code> and the dollar sign <code>$</code>.
     * 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 <code>att1-name=att1-value, att2-name = att2-value</code>
     */
    private void addByRawValueInV6(String annotName, String rval,
            Location loc) {
        final Map<String, String[]> attrs = new LinkedHashMap<String, String[]>(
                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.
     * @param loc the location information of the value for displaying better
     *            error message. Ignored if null.
     * @throws NullPointerException if val is 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<String> attrs = new ArrayList<String>();
        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.isEmpty())
                    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.isEmpty())
                    addByRawValueInV5(nm, rv);
                else
                    add(nm, null, null);
            } else {
                final String rv = (k < len
                        ? cval.substring(j, k) :
                        cval.substring(j)).trim();
                if (!rv.isEmpty())
                    addByRawValueInV5("default", rv);
            }
            j = k + 1;
        }
    }

    /**
     * @param rval <code>att1-name=att1-value, att2-name = att2-value</code>
     */
    @SuppressWarnings("unchecked")
    private void addByRawValueInV5(String annotName, String rval) {
        //The parsing of the value in format 1 is different from format 2
        final Map<String, Object> attrs = (Map) Maps.parse(null, rval, ',',
                '\'', true);
        for (Map.Entry<String, Object> me : attrs.entrySet())
            me.setValue(new String[] {(String) me.getValue()});
        //convert String to String[]
        add(annotName, (Map) attrs, null);
    }

    /**
     * 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();
    }

    /**
     * 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.
     *
     * <p>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<String, String[]> attrs;
        private final Location loc;

        private AnnotInfo(String name, Map<String, String[]> attrs,
                Location loc) {
            this.name = name;
            this.attrs = attrs;
            this.loc = loc;
        }
    }
}