/**
* Copyright 2011-2014 Asakusa Framework Team.
*
* 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.asakusafw.compiler.directio;
import java.lang.reflect.Type;
import java.text.MessageFormat;
import java.util.BitSet;
import java.util.List;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import com.asakusafw.compiler.flow.DataClass;
import com.asakusafw.compiler.flow.DataClass.Property;
import com.asakusafw.runtime.stage.directio.StringTemplate.Format;
import com.asakusafw.runtime.value.DateOption;
import com.asakusafw.runtime.value.DateTimeOption;
import com.asakusafw.utils.collections.Lists;
import com.asakusafw.utils.collections.Sets;
import com.asakusafw.vocabulary.directio.DirectFileOutputDescription;
/**
* Processes patterns in {@link DirectFileOutputDescription}.
* @since 0.2.5
* @version 0.2.6
*/
public final class OutputPattern {
static final int CHAR_BRACE_OPEN = '{';
static final int CHAR_BRACE_CLOSE = '}';
static final int CHAR_BLOCK_OPEN = '[';
static final int CHAR_BLOCK_CLOSE = ']';
static final int CHAR_WILDCARD = '*';
static final int CHAR_SEPARATE_IN_BLOCK = ':';
static final int CHAR_VARIABLE_START = '$';
static final BitSet CHAR_MAP_META = new BitSet();
static {
CHAR_MAP_META.set(0, 0x20);
CHAR_MAP_META.set('\\');
CHAR_MAP_META.set('*');
CHAR_MAP_META.set('?');
CHAR_MAP_META.set('#');
CHAR_MAP_META.set('|');
CHAR_MAP_META.set('{');
CHAR_MAP_META.set('}');
CHAR_MAP_META.set('[');
CHAR_MAP_META.set(']');
}
private static final Pattern PATTERN_ORDER = Pattern.compile(
"\\s*("
+ "(\\w+)" // 2 - asc
+ "|" + "(\\+\\s*(\\w+))" // 4 - asc
+ "|" + "(-\\s*(\\w+))" // 6 - desc
+ "|" + "((\\w+)\\s+[Aa][Ss][Cc])" // 8 - asc
+ "|" + "((\\w+)\\s+[Dd][Ee][Ss][Cc])" // 10 - desc
+ ")\\s*"
);
private static final int[] ORDER_GROUP_INDEX = { 2, 4, 6, 8, 10 };
private static final boolean[] ASC_MAP = { true, true, false, true, false };
private OutputPattern() {
return;
}
/**
* Compiles the resource pattern for the output.
* @param pattern the pattern string
* @param dataType target data type
* @return the compiled objects
* @throws IllegalArgumentException if pattern is invalid
* @see DirectFileOutputDescription#getResourcePattern()
*/
public static List<CompiledResourcePattern> compileResourcePattern(String pattern, DataClass dataType) {
if (pattern == null) {
throw new IllegalArgumentException("pattern must not be null"); //$NON-NLS-1$
}
if (dataType == null) {
throw new IllegalArgumentException("dataType must not be null"); //$NON-NLS-1$
}
List<CompiledResourcePattern> results = Lists.create();
Cursor cursor = new Cursor(pattern);
while (cursor.isEof() == false) {
if (cursor.isLiteral()) {
String literal = cursor.consumeLiteral();
results.add(new CompiledResourcePattern(literal));
} else if (cursor.isPlaceHolder()) {
Formatted ph = cursor.consumePlaceHolder();
DataClass.Property property = findProperty(dataType, (String) ph.original);
if (property == null) {
cursor.rewind();
throw new IllegalArgumentException(MessageFormat.format(
"Unknown property \"{1}\": {0}",
cursor,
ph.original));
}
String argument = ph.formatString;
Format format = findFormat(property, argument);
if (format == null) {
cursor.rewind();
throw new IllegalArgumentException(MessageFormat.format(
"Invalid format \"{1}\": {0}",
cursor,
argument == null ? "" : argument));
}
try {
format.check(property.getType(), argument);
} catch (IllegalArgumentException e) {
cursor.rewind();
throw new IllegalArgumentException(MessageFormat.format(
"Invalid format \"{1}\": {0}",
cursor,
argument == null ? "" : argument), e);
}
results.add(new CompiledResourcePattern(property, format, argument));
} else if (cursor.isRandomNumber()) {
Formatted rand = cursor.consumeRandomNumber();
RandomNumber source = (RandomNumber) rand.original;
results.add(new CompiledResourcePattern(source, Format.NATURAL, null));
} else if (cursor.isWildcard()) {
cursor.consumeWildcard();
results.add(new CompiledResourcePattern());
} else {
throw new IllegalArgumentException(MessageFormat.format(
"Invalid character: {0}",
cursor));
}
}
return results;
}
/**
* Compiled the ordering for the output.
* @param orders the each order representation
* @param dataType target data type
* @return the compiled objects
* @throws IllegalArgumentException if pattern is invalid
* @see DirectFileOutputDescription#getOrder()
*/
public static List<CompiledOrder> compileOrder(List<String> orders, DataClass dataType) {
if (orders == null) {
throw new IllegalArgumentException("orders must not be null"); //$NON-NLS-1$
}
if (dataType == null) {
throw new IllegalArgumentException("dataType must not be null"); //$NON-NLS-1$
}
Set<String> saw = Sets.create();
List<CompiledOrder> results = Lists.create();
for (String order : orders) {
boolean asc = false;
String name = null;
Matcher matcher = PATTERN_ORDER.matcher(order.trim());
if (matcher.matches() == false) {
throw new IllegalArgumentException(MessageFormat.format(
"Invalid order format: {0}",
order));
}
for (int i = 0; i < ORDER_GROUP_INDEX.length; i++) {
int groupIndex = ORDER_GROUP_INDEX[i];
if (matcher.group(groupIndex) != null) {
asc = ASC_MAP[i];
name = matcher.group(groupIndex);
break;
}
}
assert name != null;
DataClass.Property property = findProperty(dataType, name);
if (property == null) {
throw new IllegalArgumentException(MessageFormat.format(
"Unknown property \"{1}\": {0}",
order,
name));
}
if (saw.contains(property.getName())) {
throw new IllegalArgumentException(MessageFormat.format(
"Duplicate property \"{1}\": {0}",
order,
name));
}
saw.add(property.getName());
results.add(new CompiledOrder(property, asc));
}
return results;
}
private static Property findProperty(DataClass dataType, String name) {
assert dataType != null;
assert name != null;
return dataType.findProperty(name);
}
private static Format findFormat(Property property, String argument) {
if (argument == null) {
return Format.NATURAL;
}
Type type = property.getType();
if (type == DateOption.class) {
return Format.DATE;
}
if (type == DateTimeOption.class) {
return Format.DATETIME;
}
return null;
}
private static final class Cursor {
private final char[] cbuf;
private int lastSegmentPosition;
private int position;
Cursor(String value) {
assert value != null;
this.cbuf = value.toCharArray();
this.position = 0;
}
boolean isEof() {
return cbuf.length == position;
}
boolean isLiteral() {
if (isEof()) {
return false;
}
return CHAR_MAP_META.get(cbuf[position]) == false;
}
boolean isPlaceHolder() {
if (isEof()) {
return false;
}
return cbuf[position] == CHAR_BRACE_OPEN;
}
boolean isRandomNumber() {
if (isEof()) {
return false;
}
return cbuf[position] == CHAR_BLOCK_OPEN;
}
boolean isWildcard() {
if (isEof()) {
return false;
}
return cbuf[position] == CHAR_WILDCARD;
}
void rewind() {
this.position = lastSegmentPosition;
}
String consumeLiteral() {
assert isLiteral();
this.lastSegmentPosition = position;
int start = position;
while (isLiteral()) {
char c = cbuf[position];
if (c == CHAR_VARIABLE_START) {
skipVariable();
} else if (CHAR_MAP_META.get(c) == false) {
advance();
} else {
throw new AssertionError(c);
}
}
return String.valueOf(cbuf, start, position - start);
}
private void skipVariable() {
int start = position;
assert cbuf[position] == CHAR_VARIABLE_START;
advance();
if (isEof() || cbuf[position] != CHAR_BRACE_OPEN) {
return;
}
advance();
while (true) {
if (isEof()) {
position = start;
throw new IllegalArgumentException(MessageFormat.format(
"Variable is not closed: {0}",
this));
}
char c = cbuf[position];
if (c == CHAR_BRACE_CLOSE) {
break;
}
advance();
}
advance();
}
Formatted consumePlaceHolder() {
assert isPlaceHolder();
this.lastSegmentPosition = position;
int start = position + 1;
String propertyName;
String formatString;
advance();
while (true) {
if (isEof()) {
position = start;
throw new IllegalArgumentException(MessageFormat.format(
"Placeholder is not closed: {0}",
this));
}
char c = cbuf[position];
if (c == CHAR_BRACE_CLOSE || c == CHAR_SEPARATE_IN_BLOCK) {
break;
}
advance();
}
propertyName = String.valueOf(cbuf, start, position - start);
if (cbuf[position] == CHAR_SEPARATE_IN_BLOCK) {
advance();
int formatStart = position;
while (true) {
if (isEof()) {
position = start;
throw new IllegalArgumentException(MessageFormat.format(
"Placeholder is not closed: {0}",
this));
}
char c = cbuf[position];
if (c == CHAR_BRACE_CLOSE) {
break;
}
advance();
}
formatString = String.valueOf(cbuf, formatStart, position - formatStart);
} else {
formatString = null;
}
assert cbuf[position] == CHAR_BRACE_CLOSE;
advance();
return new Formatted(propertyName, formatString);
}
private static final Pattern RNG = Pattern.compile("(\\d+)\\.{2,3}(\\d+)(:(.*))?");
Formatted consumeRandomNumber() {
assert isRandomNumber();
this.lastSegmentPosition = position;
int start = position + 1;
while (true) {
if (isEof()) {
position = start;
throw new IllegalArgumentException(MessageFormat.format(
"Random number is not closed: {0}",
this));
}
char c = cbuf[position];
if (c == CHAR_BLOCK_CLOSE) {
break;
}
advance();
}
String content = String.valueOf(cbuf, start, position - start);
Matcher matcher = RNG.matcher(content);
if (matcher.matches() == false) {
position = start;
throw new IllegalArgumentException(MessageFormat.format(
"Invalid random number format: {0}",
this));
}
int lower;
try {
lower = Integer.parseInt(matcher.group(1));
} catch (NumberFormatException e) {
position = start + matcher.start(1);
throw new IllegalArgumentException(MessageFormat.format(
"Invalid random number format: {0}",
this), e);
}
int upper;
try {
upper = Integer.parseInt(matcher.group(2));
} catch (NumberFormatException e) {
position = start + matcher.start(2);
throw new IllegalArgumentException(MessageFormat.format(
"Invalid random number format: {0}",
this), e);
}
if (lower >= upper) {
position = start + matcher.start(1);
throw new IllegalArgumentException(MessageFormat.format(
"The random number [lower..upper] must be lower < upper: {0}",
this));
}
String format = matcher.group(4);
advance();
return new Formatted(new RandomNumber(lower, upper), format);
}
void consumeWildcard() {
assert isWildcard();
advance();
}
private void advance() {
position = Math.min(position + 1, cbuf.length);
}
@Override
public String toString() {
StringBuilder buf = new StringBuilder();
buf.append('[');
for (int i = 0, n = position; i < n; i++) {
buf.append(cbuf[i]);
}
buf.append(" >> ");
for (int i = position, n = cbuf.length; i < n; i++) {
buf.append(cbuf[i]);
}
buf.append(']');
return buf.toString();
}
}
private static class Formatted {
final Object original;
final String formatString;
Formatted(Object original, String formatString) {
this.original = original;
this.formatString = formatString;
}
}
/**
* The compiled resource pattern.
* @since 0.2.5
*/
public static final class CompiledResourcePattern {
private final SourceKind kind;
private final Object source;
private final Format format;
private final String argument;
/**
* Creates a new wildcard.
* @since 0.4.0
*/
public CompiledResourcePattern() {
this.kind = SourceKind.ENVIRONMENT;
this.source = null;
this.format = Format.PLAIN;
this.argument = null;
}
/**
* Creates a new literal.
* @param string literal value
* @throws IllegalArgumentException if some parameters were {@code null}
*/
public CompiledResourcePattern(String string) {
if (string == null) {
throw new IllegalArgumentException("string must not be null"); //$NON-NLS-1$
}
this.kind = SourceKind.NOTHING;
this.source = null;
this.format = Format.PLAIN;
this.argument = string;
}
/**
* Creates a new instance.
* @param target target property
* @param format format kind
* @param argument format argument (nullable)
* @throws IllegalArgumentException if some parameters were {@code null}
*/
public CompiledResourcePattern(DataClass.Property target, Format format, String argument) {
if (target == null) {
throw new IllegalArgumentException("target must not be null"); //$NON-NLS-1$
}
if (format == null) {
throw new IllegalArgumentException("format must not be null"); //$NON-NLS-1$
}
this.kind = SourceKind.PROPERTY;
this.source = target;
this.format = format;
this.argument = argument;
format.check(target.getType(), argument);
}
/**
* Creates a new instance.
* @param source the source object
* @param format format kind
* @param argument format argument (nullable)
* @throws IllegalArgumentException if some parameters were {@code null}
*/
public CompiledResourcePattern(RandomNumber source, Format format, String argument) {
if (source == null) {
throw new IllegalArgumentException("source must not be null"); //$NON-NLS-1$
}
if (format == null) {
throw new IllegalArgumentException("format must not be null"); //$NON-NLS-1$
}
this.kind = SourceKind.RANDOM;
this.source = source;
this.format = format;
this.argument = argument;
}
/**
* Returns the kind of the souce of this fragment.
* @return the kind
* @since 0.2.6
*/
public SourceKind getKind() {
return kind;
}
/**
* Returns the source of this fragment.
* @return the source, or {@code null} if the source is not specified
* @since 0.2.6
*/
public Object getSource() {
return source;
}
/**
* Returns the target property.
* @return the target property, or {@code null} if the source is not a property
* @see #getSource()
*/
public DataClass.Property getTarget() {
if (kind != SourceKind.PROPERTY) {
return null;
}
return (DataClass.Property) source;
}
/**
* Returns the random number specification.
* @return the random number spec, or {@code null} if the source is not a random number
* @see #getSource()
* @since 0.2.6
*/
public RandomNumber getRandomNumber() {
if (kind != SourceKind.RANDOM) {
return null;
}
return (RandomNumber) source;
}
/**
* Returns the format kind.
* @return the format
*/
public Format getFormat() {
return format;
}
/**
* Returns the argument for the format.
* @return the argument, or {@code null} if not defined
*/
public String getArgument() {
return argument;
}
}
/**
* The compiled ordering pattern.
* @since 0.2.5
*/
public static final class CompiledOrder {
private final DataClass.Property target;
private final boolean ascend;
/**
* Creates a new instance.
* @param target target property
* @param ascend whether the ordering is ascend
* @throws IllegalArgumentException if some parameters were {@code null}
*/
public CompiledOrder(DataClass.Property target, boolean ascend) {
if (target == null) {
throw new IllegalArgumentException("target must not be null"); //$NON-NLS-1$
}
this.target = target;
this.ascend = ascend;
}
/**
* Returns the target property.
* @return the target property
*/
public DataClass.Property getTarget() {
return target;
}
/**
* Returns whether the ordering is ascend.
* @return {@code true} for ascend, or {@code false} for descend
*/
public boolean isAscend() {
return ascend;
}
}
/**
* The source kind.
* @since 0.2.6
*/
public enum SourceKind {
/**
* Source is nothing (for literals).
*/
NOTHING,
/**
* Source is a property.
* @see com.asakusafw.compiler.flow.DataClass.Property
*/
PROPERTY,
/**
* Source is a random number generator.
* @see RandomNumber
*/
RANDOM,
/**
* Source is from current Environment ID.
* @since 0.4.0
*/
ENVIRONMENT,
}
/**
* Represents a random number.
* @since 0.2.6
*/
public static class RandomNumber {
private final int lowerBound;
private final int upperBound;
/**
* Creates a new instance.
* @param lowerBound the lower bound (inclusive)
* @param upperBound the upper bound (inclusive)
*/
public RandomNumber(int lowerBound, int upperBound) {
this.lowerBound = lowerBound;
this.upperBound = upperBound;
}
/**
* Returns the lower bound of this random number.
* @return the lower bound (inclusive)
*/
public int getLowerBound() {
return lowerBound;
}
/**
* Returns the upper bound of this random number.
* @return the upper bound (inclusive)
*/
public int getUpperBound() {
return upperBound;
}
}
}