| /* |
| * Copyright 2009 Google Inc. |
| * |
| * Licensed under the Apache License, Version 2.0 (the "License"); you may not |
| * use this file except in compliance with the License. You may obtain a copy of |
| * the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT |
| * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the |
| * License for the specific language governing permissions and limitations under |
| * the License. |
| */ |
| package com.google.gwt.uibinder.attributeparsers; |
| |
| import com.google.gwt.core.ext.typeinfo.JType; |
| import com.google.gwt.uibinder.rebind.FieldManager; |
| import com.google.gwt.uibinder.rebind.XMLElement; |
| |
| import java.util.regex.Matcher; |
| import java.util.regex.Pattern; |
| |
| /** |
| * Deals with field references, e.g. the bits in braces here: <code><div |
| * class="{style.enabled} fancy {style.impressive}" /></code>, by converting |
| * them to java expressions (with the help of a |
| * {@link com.google.gwt.uibinder.attributeparsers.FieldReferenceConverter.Delegate |
| * Delegate}). |
| * <p> |
| * A field reference is one or more segments separated by dots. The first |
| * segment is considered to be a reference to a ui field, and succeeding |
| * segments are method calls. So, <code>"{able.baker.charlie}"</code> becomes |
| * <code>"able.baker().charlie()"</code>. |
| * <p> |
| * A field reference starts with '{' and is followed immediately by a character |
| * that can legally start a java identifier—that is a letter, $, or |
| * underscore. Braces not followed by such a character are left in place. |
| * <p> |
| * For convenience when dealing with generated CssResources, field segments with |
| * dashes are converted to camel case. That is, {able.baker-charlie} is the same |
| * as {able.bakerCharlie} |
| * <p> |
| * Double mustaches (i.e. "{{..}}") are not matched as references to play well |
| * with modern templating systems. |
| * <p> |
| * Opening braces may be escape by slash. That is, "\{foo}" will converted to |
| * "{foo}", with no field reference detected. |
| */ |
| public class FieldReferenceConverter { |
| /** |
| * May be thrown by the |
| * {@link com.google.gwt.uibinder.attributeparsers.FieldReferenceConverter.Delegate |
| * Delegate} for badly formatted input. |
| */ |
| @SuppressWarnings("serial") |
| public static class IllegalFieldReferenceException extends RuntimeException { |
| } |
| |
| /** |
| * Responsible for the bits around and between the field references. May throw |
| * IllegalFieldReferenceException as it sees fit. |
| */ |
| interface Delegate { |
| /** |
| * Returns the types any parsed field references are expected to return. |
| * Multiple values indicates an overload. E.g., in <a href={...}> either a |
| * String or a SafeUri is allowed. |
| */ |
| JType[] getTypes(); |
| |
| /** |
| * Called for fragment around and between field references. |
| * <p> |
| * Note that it will be called with empty strings if these surrounding bits |
| * are empty. E.g., "{style.enabled} fancy {style.impressive}" would call |
| * this method three times, with "", " fancy ", and "". |
| * <p> |
| * A string with no field references is treated as a single fragment, and |
| * causes a single call to this method. |
| */ |
| String handleFragment(String fragment) throws IllegalFieldReferenceException; |
| |
| /** |
| * Called for each expanded field reference, to allow it to be stitched |
| * together with surrounding fragments. |
| */ |
| String handleReference(String reference) throws IllegalFieldReferenceException; |
| } |
| |
| /** |
| * Used by {@link FieldReferenceConverter#countFieldReferences}. Passthrough |
| * implementation that counts the number of calls handleReference has been called, |
| * so that we know how many field references a given string contains. |
| */ |
| private static final class Telltale implements FieldReferenceConverter.Delegate { |
| private int computedCount; |
| |
| public int getComputedCount() { |
| return computedCount; |
| } |
| |
| public JType[] getTypes() { |
| return new JType[0]; |
| } |
| |
| public String handleFragment(String fragment) { |
| return fragment; |
| } |
| |
| public String handleReference(String reference) { |
| computedCount++; |
| return reference; |
| } |
| } |
| |
| private static final Pattern BRACES = Pattern.compile("[{]([^}]*)[}]"); |
| private static final Pattern LEGAL_FIRST_CHAR = Pattern.compile("^[$_a-zA-Z].*"); |
| private static final String DOTS_AND_PARENS = "[().]"; |
| |
| /** |
| * Returns the number of field references in the given string. |
| */ |
| public static int countFieldReferences(String string) { |
| Telltale telltale = new Telltale(); |
| new FieldReferenceConverter(null).convert(null, string, telltale); |
| return telltale.getComputedCount(); |
| } |
| |
| /** |
| * Reverses most of the work of {@link #convert}, turning a java expression |
| * back into a dotted path. |
| */ |
| public static String expressionToPath(String expression) { |
| String[] chunks = expression.split(DOTS_AND_PARENS); |
| StringBuilder b = new StringBuilder(); |
| for (String chunk : chunks) { |
| if (b.length() > 0 && chunk.length() > 0) { |
| b.append("."); |
| } |
| b.append(chunk); |
| } |
| return b.toString(); |
| } |
| |
| /** |
| * Returns true if the given string holds one or more field references. |
| */ |
| public static boolean hasFieldReferences(String string) { |
| return countFieldReferences(string) > 0; |
| } |
| |
| private final CssNameConverter cssConverter = new CssNameConverter(); |
| private final FieldManager fieldManager; |
| |
| /** |
| * @param fieldManager to register parsed references with. May be null |
| */ |
| FieldReferenceConverter(FieldManager fieldManager) { |
| this.fieldManager = fieldManager; |
| } |
| |
| /** |
| * @throws IllegalFieldReferenceException if the delegate does |
| */ |
| public String convert(String in, Delegate delegate) { |
| return convert(null, in, delegate); |
| } |
| |
| /** |
| * @throws IllegalFieldReferenceException if the delegate does |
| */ |
| public String convert(XMLElement source, String in, Delegate delegate) { |
| StringBuilder b = new StringBuilder(); |
| int nextFindStart = 0; |
| int lastMatchEnd = 0; |
| |
| Matcher m = BRACES.matcher(in); |
| while (m.find(nextFindStart)) { |
| String fieldReference = m.group(1); |
| int start = m.start(); |
| if (!isLegalPreviousCharacter(in, start)) { |
| nextFindStart = start + 1; |
| continue; |
| } |
| if (!isLegalFirstCharacter(fieldReference)) { |
| nextFindStart = start + 2; |
| continue; |
| } |
| |
| String precedingFragment = in.substring(lastMatchEnd, start); |
| precedingFragment = handleFragment(precedingFragment, delegate); |
| b.append(precedingFragment); |
| |
| if (fieldManager != null) { |
| fieldManager.registerFieldReference(source, fieldReference, delegate.getTypes()); |
| } |
| fieldReference = expandDots(fieldReference); |
| b.append(delegate.handleReference(fieldReference)); |
| nextFindStart = lastMatchEnd = m.end(); |
| } |
| |
| b.append(handleFragment(in.substring(lastMatchEnd), delegate)); |
| return b.toString(); |
| } |
| |
| private String expandDots(String value) { |
| StringBuilder b = new StringBuilder(); |
| String[] segments = value.split("[.]"); |
| |
| for (int i = 0; i < segments.length; ++i) { |
| String segment = cssConverter.convertName(segments[i]); |
| |
| // The first segment is converted to a field getter. So, |
| // "bundle.whatever" becomes "get_bundle().whatever". |
| if (fieldManager != null && i == 0) { |
| segment = fieldManager.convertFieldToGetter(segment); |
| } |
| |
| if (b.length() == 0) { |
| b.append(segment); // field name |
| } else { |
| b.append(".").append(segment).append("()"); |
| } |
| } |
| return b.toString(); |
| } |
| |
| private String handleFragment(String fragment, Delegate delegate) { |
| fragment = fragment.replace("\\{", "{"); |
| return delegate.handleFragment(fragment); |
| } |
| |
| private boolean isLegalFirstCharacter(String fieldReference) { |
| return LEGAL_FIRST_CHAR.matcher(fieldReference).matches(); |
| } |
| |
| private boolean isLegalPreviousCharacter(String in, int start) { |
| if (start < 1) { |
| return true; |
| } |
| char previousChar = in.charAt(start - 1); |
| return previousChar != '{' && previousChar != '\\'; |
| } |
| } |