| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You 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 org.apache.bval.jsr303.util; |
| |
| import java.io.StringWriter; |
| import java.text.ParsePosition; |
| |
| import javax.validation.ValidationException; |
| |
| import org.apache.commons.lang3.StringEscapeUtils; |
| |
| /** |
| * Defines a path navigation algorithm and a means of interacting with same. |
| * |
| * @version $Rev: 1136233 $ $Date: 2011-06-15 17:49:27 -0500 (Wed, 15 Jun 2011) $ |
| */ |
| public class PathNavigation { |
| |
| /** |
| * Path traversal callback function interface. |
| */ |
| public interface Callback<T> { |
| /** |
| * Handle a .-delimited property. |
| * |
| * @param name |
| */ |
| void handleProperty(String name); |
| |
| /** |
| * Handle an index or key embedded in []. |
| * |
| * @param value |
| */ |
| void handleIndexOrKey(String value); |
| |
| /** |
| * Handle contiguous []. |
| */ |
| void handleGenericInIterable(); |
| |
| /** |
| * Return a result. Called after navigation is complete. |
| * |
| * @return result |
| */ |
| T result(); |
| } |
| |
| /** |
| * Callback "procedure" that always returns null. |
| */ |
| public static abstract class CallbackProcedure implements Callback<Object> { |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public final Object result() { |
| complete(); |
| return null; |
| } |
| |
| /** |
| * Complete this CallbackProcedure. Default implementation is noop. |
| */ |
| protected void complete() { |
| } |
| } |
| |
| /** |
| * Create a new PathNavigation instance. |
| */ |
| private PathNavigation() { |
| } |
| |
| /** |
| * Navigate a path using the specified callback, returning its result. |
| * |
| * @param <T> |
| * @param propertyPath |
| * , null is assumed empty/root |
| * @param callback |
| * @return T result |
| */ |
| public static <T> T navigateAndReturn(CharSequence propertyPath, Callback<? extends T> callback) { |
| try { |
| parse(propertyPath == null ? "" : propertyPath, new PathPosition(callback)); |
| } catch (ValidationException ex) { |
| throw ex; |
| } catch (Exception ex) { |
| throw new ValidationException(String.format("invalid property: %s", propertyPath), ex); |
| } |
| return callback.result(); |
| } |
| |
| /** |
| * Navigate a path using the specified callback. |
| * |
| * @param propertyPath |
| * @param callback |
| */ |
| public static void navigate(CharSequence propertyPath, Callback<?> callback) { |
| navigateAndReturn(propertyPath, callback); |
| } |
| |
| private static void parse(CharSequence path, PathPosition pos) throws Exception { |
| int len = path.length(); |
| boolean sep = true; |
| while (pos.getIndex() < len) { |
| int here = pos.getIndex(); |
| char c = path.charAt(here); |
| switch (c) { |
| case ']': |
| throw new IllegalStateException(String.format("Position %s: unexpected '%s'", here, c)); |
| case '[': |
| handleIndex(path, pos.next()); |
| break; |
| case '.': |
| if (sep) { |
| throw new IllegalStateException(String.format("Position %s: expected property, index/key, or end of expression", here)); |
| } |
| sep = true; |
| pos.next(); |
| // fall through: |
| default: |
| if (!sep) { |
| throw new IllegalStateException(String.format("Position %s: expected property path separator, index/key, or end of expression", here)); |
| } |
| pos.handleProperty(parseProperty(path, pos)); |
| } |
| sep = false; |
| } |
| } |
| |
| private static String parseProperty(CharSequence path, PathPosition pos) throws Exception { |
| int len = path.length(); |
| int start = pos.getIndex(); |
| loop: while (pos.getIndex() < len) { |
| switch (path.charAt(pos.getIndex())) { |
| case '[': |
| case ']': |
| case '.': |
| break loop; |
| } |
| pos.next(); |
| } |
| if (pos.getIndex() > start) { |
| return path.subSequence(start, pos.getIndex()).toString(); |
| } |
| throw new IllegalStateException(String.format("Position %s: expected property", start)); |
| } |
| |
| /** |
| * Handles an index/key. If the text contained between [] is surrounded by a pair of " or ', it will be treated as a |
| * string which may contain Java escape sequences. |
| * |
| * @param path |
| * @param pos |
| * @throws Exception |
| */ |
| private static void handleIndex(CharSequence path, PathPosition pos) throws Exception { |
| int len = path.length(); |
| int start = pos.getIndex(); |
| if (start < len) { |
| char first = path.charAt(pos.getIndex()); |
| if (first == '"' || first == '\'') { |
| String s = parseQuotedString(path, pos); |
| if (s != null && path.charAt(pos.getIndex()) == ']') { |
| pos.handleIndexOrKey(s); |
| pos.next(); |
| return; |
| } |
| } |
| // no quoted string; match ] greedily |
| while (pos.getIndex() < len) { |
| int here = pos.getIndex(); |
| try { |
| if (path.charAt(here) == ']') { |
| if (here == start) { |
| pos.handleGenericInIterable(); |
| } else { |
| pos.handleIndexOrKey(path.subSequence(start, here).toString()); |
| } |
| return; |
| } |
| } finally { |
| pos.next(); |
| } |
| } |
| } |
| throw new IllegalStateException(String.format("Position %s: unparsable index", start)); |
| } |
| |
| private static String parseQuotedString(CharSequence path, PathPosition pos) throws Exception { |
| int len = path.length(); |
| int start = pos.getIndex(); |
| if (start < len) { |
| char quote = path.charAt(start); |
| pos.next(); |
| StringWriter w = new StringWriter(); |
| while (pos.getIndex() < len) { |
| int here = pos.getIndex(); |
| // look for matching quote |
| if (path.charAt(here) == quote) { |
| pos.next(); |
| return w.toString(); |
| } |
| int codePoints = StringEscapeUtils.UNESCAPE_JAVA.translate(path, here, w); |
| if (codePoints == 0) { |
| w.write(Character.toChars(Character.codePointAt(path, here))); |
| pos.next(); |
| } else { |
| for (int i = 0; i < codePoints; i++) { |
| pos.plus(Character.charCount(Character.codePointAt(path, pos.getIndex()))); |
| } |
| } |
| } |
| // if reached, reset due to no ending quote found |
| pos.setIndex(start); |
| } |
| return null; |
| } |
| |
| /** |
| * ParsePosition/Callback |
| */ |
| private static class PathPosition extends ParsePosition implements Callback<Object> { |
| final Callback<?> delegate; |
| |
| /** |
| * Create a new {@link PathPosition} instance. |
| * |
| * @param delegate |
| */ |
| private PathPosition(Callback<?> delegate) { |
| super(0); |
| this.delegate = delegate; |
| } |
| |
| /** |
| * Increment and return this. |
| * |
| * @return this |
| */ |
| public PathPosition next() { |
| return plus(1); |
| } |
| |
| /** |
| * Increase position and return this. |
| * |
| * @param addend |
| * @return this |
| */ |
| public PathPosition plus(int addend) { |
| setIndex(getIndex() + addend); |
| return this; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void handleProperty(String name) { |
| delegate.handleProperty(name); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void handleIndexOrKey(String value) { |
| delegate.handleIndexOrKey(value); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public void handleGenericInIterable() { |
| delegate.handleGenericInIterable(); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| public Object result() { |
| return null; |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| /* |
| * Override equals to make findbugs happy; |
| * would simply ignore but doesn't seem to be possible at the inner class level |
| * without attaching the filter to the containing class. |
| */ |
| @Override |
| public boolean equals(Object obj) { |
| return super.equals(obj); |
| } |
| |
| /** |
| * {@inheritDoc} |
| */ |
| /* |
| * Override hashCode to make findbugs happy in the presence of overridden #equals :P |
| */ |
| @Override |
| public int hashCode() { |
| return super.hashCode(); |
| } |
| } |
| |
| } |