<author email="javajedi@users.sf.net">Tim McCune</author>
</properties>
<body>
+ <release version="3.0.1" date="TBD">
+ <action dev="jahlborn" type="update">
+ Add ColumnFormatter utility which can apply Column "Format" property
+ for display of column values.
+ </action>
+ </release>
<release version="3.0.0" date="2019-02-08" description="Update to Java 8">
<action dev="jahlborn" type="update">
Jackcess now requires a Java 8+ runtime. As part of this update, all
*
* <table border="1" width="25%" cellpadding="3" cellspacing="0">
* <tr class="TableHeadingColor" align="left"><th>Function</th><th>Supported</th></tr>
- * <tr class="TableRowColor"><td>Format[$]</td><td>Partial</td></tr>
+ * <tr class="TableRowColor"><td>Format[$]</td><td>Y</td></tr>
* <tr class="TableRowColor"><td>InStr</td><td>Y</td></tr>
* <tr class="TableRowColor"><td>InStrRev</td><td>Y</td></tr>
* <tr class="TableRowColor"><td>LCase[$]</td><td>Y</td></tr>
package com.healthmarketscience.jackcess.impl;
+import com.healthmarketscience.jackcess.expr.Value;
+
protected String withErrorContext(String msg) {
return _col.withErrorContext(msg);
}
+
+ protected Value toValue(Object val) {
+ return toValue(val, _col.getType());
+ }
}
@Override
public Value getThisColumnValue() {
- return toValue(_val, getCol().getType());
+ return toValue(_val);
}
@Override
import com.healthmarketscience.jackcess.expr.EvalException;
import com.healthmarketscience.jackcess.expr.Function;
import com.healthmarketscience.jackcess.expr.FunctionLookup;
+import com.healthmarketscience.jackcess.expr.LocaleContext;
import com.healthmarketscience.jackcess.expr.NumericConfig;
import com.healthmarketscience.jackcess.expr.TemporalConfig;
import com.healthmarketscience.jackcess.expr.Value;
}
});
- private static boolean stringIsNumeric(EvalContext ctx, Value param) {
+ private static boolean stringIsNumeric(LocaleContext ctx, Value param) {
return (maybeGetAsBigDecimal(ctx, param) != null);
}
- static BigDecimal maybeGetAsBigDecimal(EvalContext ctx, Value param) {
+ static BigDecimal maybeGetAsBigDecimal(LocaleContext ctx, Value param) {
try {
return param.getAsBigDecimal(ctx);
} catch(EvalException ignored) {
return (maybeGetAsDateTimeValue(ctx, param) != null);
}
- static Value maybeGetAsDateTimeValue(EvalContext ctx, Value param) {
+ static Value maybeGetAsDateTimeValue(LocaleContext ctx, Value param) {
try {
// see if we can coerce to date/time
return param.getAsDateTimeValue(ctx);
import com.healthmarketscience.jackcess.expr.TemporalConfig;
import com.healthmarketscience.jackcess.expr.Value;
import com.healthmarketscience.jackcess.impl.ColumnImpl;
+import org.apache.commons.lang3.StringUtils;
/**
exprStr = exprStr.trim();
}
- if((exprStr == null) || (exprStr.length() == 0)) {
+ if(StringUtils.isEmpty(exprStr)) {
return null;
}
import com.healthmarketscience.jackcess.expr.NumericConfig;
import com.healthmarketscience.jackcess.expr.TemporalConfig;
import com.healthmarketscience.jackcess.expr.Value;
+import org.apache.commons.lang3.StringUtils;
import static com.healthmarketscience.jackcess.impl.expr.ExpressionTokenizer.ExprBuf;
/**
private static final Map<String,Fmt> PREDEF_FMTS = new HashMap<String,Fmt>();
static {
- PREDEF_FMTS.put("General Date", args -> ValueSupport.toValue(
+ putPredefFormat("General Date", args -> ValueSupport.toValue(
args.coerceToDateTimeValue().getAsString()));
- PREDEF_FMTS.put("Long Date",
+ putPredefFormat("Long Date",
new PredefDateFmt(TemporalConfig.Type.LONG_DATE));
- PREDEF_FMTS.put("Medium Date",
+ putPredefFormat("Medium Date",
new PredefDateFmt(TemporalConfig.Type.MEDIUM_DATE));
- PREDEF_FMTS.put("Short Date",
+ putPredefFormat("Short Date",
new PredefDateFmt(TemporalConfig.Type.SHORT_DATE));
- PREDEF_FMTS.put("Long Time",
+ putPredefFormat("Long Time",
new PredefDateFmt(TemporalConfig.Type.LONG_TIME));
- PREDEF_FMTS.put("Medium Time",
+ putPredefFormat("Medium Time",
new PredefDateFmt(TemporalConfig.Type.MEDIUM_TIME));
- PREDEF_FMTS.put("Short Time",
+ putPredefFormat("Short Time",
new PredefDateFmt(TemporalConfig.Type.SHORT_TIME));
- PREDEF_FMTS.put("General Number", args -> ValueSupport.toValue(
+ putPredefFormat("General Number", args -> ValueSupport.toValue(
args.coerceToNumberValue().getAsString()));
- PREDEF_FMTS.put("Currency",
+ putPredefFormat("Currency",
new PredefNumberFmt(NumericConfig.Type.CURRENCY));
- PREDEF_FMTS.put("Euro", new PredefNumberFmt(NumericConfig.Type.EURO));
- PREDEF_FMTS.put("Fixed",
+ putPredefFormat("Euro", new PredefNumberFmt(NumericConfig.Type.EURO));
+ putPredefFormat("Fixed",
new PredefNumberFmt(NumericConfig.Type.FIXED));
- PREDEF_FMTS.put("Standard",
+ putPredefFormat("Standard",
new PredefNumberFmt(NumericConfig.Type.STANDARD));
- PREDEF_FMTS.put("Percent",
+ putPredefFormat("Percent",
new PredefNumberFmt(NumericConfig.Type.PERCENT));
- PREDEF_FMTS.put("Scientific", new ScientificPredefNumberFmt());
+ putPredefFormat("Scientific", new ScientificPredefNumberFmt());
- PREDEF_FMTS.put("True/False", new PredefBoolFmt("True", "False"));
- PREDEF_FMTS.put("Yes/No", new PredefBoolFmt("Yes", "No"));
- PREDEF_FMTS.put("On/Off", new PredefBoolFmt("On", "Off"));
+ putPredefFormat("True/False", new PredefBoolFmt("True", "False"));
+ putPredefFormat("Yes/No", new PredefBoolFmt("Yes", "No"));
+ putPredefFormat("On/Off", new PredefBoolFmt("On", "Off"));
}
private static final Fmt NULL_FMT = args -> ValueSupport.EMPTY_STR_VAL;
+ private static final Fmt DUMMY_FMT = args -> args.getNonNullExpr();
private static final char QUOTE_CHAR = '"';
private static final char ESCAPE_CHAR = '\\';
_firstWeekType = firstWeekType;
}
+ public Args setExpr(Value expr) {
+ _expr = expr;
+ return this;
+ }
+
+ public Value getNonNullExpr() {
+ return (_expr.isNull() ? ValueSupport.EMPTY_STR_VAL : _expr);
+ }
+
public boolean isNullOrEmptyString() {
return(_expr.isNull() ||
// only a string value could ever be an empty string
public String getAsString() {
return _expr.getAsString(_ctx);
}
+
+ public Value format(Fmt fmt) {
+ Value origExpr = _expr;
+ try {
+ return fmt.format(this);
+ } catch(EvalException ee) {
+ // values which cannot be formatted as the target type are just
+ // returned "as is"
+ return origExpr;
+ }
+ }
}
private FormatUtil() {}
+ /**
+ * Utility for leveraging format support outside of expression evaluation.
+ */
+ public static class StandaloneFormatter
+ {
+ private final Fmt _fmt;
+ private final Args _args;
+
+ private StandaloneFormatter(Fmt fmt, Args args) {
+ _fmt = fmt;
+ _args = args;
+ }
+
+ public Value format(Value expr) {
+ return _args.setExpr(expr).format(_fmt);
+ }
+ }
public static Value format(EvalContext ctx, Value expr, String fmtStr,
int firstDay, int firstWeekType) {
+ Args args = new Args(ctx, expr, firstDay, firstWeekType);
+ return args.format(createFormat(args, fmtStr));
+ }
- try {
- Args args = new Args(ctx, expr, firstDay, firstWeekType);
+ public static StandaloneFormatter createStandaloneFormatter(
+ EvalContext ctx, String fmtStr, int firstDay, int firstWeekType) {
+ Args args = new Args(ctx, null, firstDay, firstWeekType);
+ Fmt fmt = createFormat(args, fmtStr);
+ return new StandaloneFormatter(fmt, args);
+ }
- Fmt predefFmt = PREDEF_FMTS.get(fmtStr);
- if(predefFmt != null) {
- if(args.isNullOrEmptyString()) {
- // predefined formats return empty string for null
- return ValueSupport.EMPTY_STR_VAL;
- }
- return predefFmt.format(args);
- }
+ private static Fmt createFormat(Args args, String fmtStr) {
+ Fmt predefFmt = PREDEF_FMTS.get(fmtStr);
+ if(predefFmt != null) {
+ return predefFmt;
+ }
- // TODO implement caching for custom formats? put into Bindings. use
- // special "cache" prefix to know which caches to clear when evalconfig
- // is altered (could also cache other Format* functions)
+ if(StringUtils.isEmpty(fmtStr)) {
+ return DUMMY_FMT;
+ }
- return parseCustomFormat(fmtStr, args).format(args);
+ // TODO implement caching for custom formats? put into Bindings. use
+ // special "cache" prefix to know which caches to clear when evalconfig
+ // is altered (could also cache other Format* functions)
- } catch(EvalException ee) {
- // values which cannot be formatted as the target type are just
- // returned "as is"
- return expr;
- }
+ return parseCustomFormat(fmtStr, args);
}
private static Fmt parseCustomFormat(String fmtStr, Args args) {
}
}
+ private static void putPredefFormat(String key, Fmt fmt) {
+ // predefined formats return empty string for null
+ Fmt wrapFmt = args -> (args.isNullOrEmptyString() ?
+ ValueSupport.EMPTY_STR_VAL :
+ fmt.format(args));
+ PREDEF_FMTS.put(key, wrapFmt);
+ }
+
private static final class PredefDateFmt implements Fmt
{
private final TemporalConfig.Type _type;
--- /dev/null
+/*
+Copyright (c) 2019 James Ahlborn
+
+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.healthmarketscience.jackcess.util;
+
+import java.io.IOException;
+import java.util.Map;
+
+import com.healthmarketscience.jackcess.Column;
+import com.healthmarketscience.jackcess.PropertyMap;
+import com.healthmarketscience.jackcess.expr.EvalConfig;
+import com.healthmarketscience.jackcess.expr.EvalException;
+import com.healthmarketscience.jackcess.impl.ColEvalContext;
+import com.healthmarketscience.jackcess.impl.ColumnImpl;
+import com.healthmarketscience.jackcess.impl.expr.FormatUtil;
+import org.apache.commons.lang3.StringUtils;
+
+/**
+ * Utility for applying Column formatting to column values for display. This
+ * utility loads the "Format" property from the given column and builds an
+ * appropriate formatter (essentially leveraging the internals of the
+ * expression execution engine's support for the "Format()" function). Since
+ * formats leverage the expression evaluation engine, the underlying
+ * Database's {@link EvalConfig} can be used to alter how this utility formats
+ * values. Note, formatted values may be suitable for <i>display only</i>
+ * (i.e. a formatted value may not be accepted as an input value to a Table
+ * add/update method).
+ *
+ * @author James Ahlborn
+ * @usage _general_class_
+ */
+public class ColumnFormatter
+{
+ private final ColumnImpl _col;
+ private final FormatEvalContext _ctx;
+ private String _fmtStr;
+ private FormatUtil.StandaloneFormatter _fmt;
+
+ public ColumnFormatter(Column col) throws IOException {
+ _col = (ColumnImpl)col;
+ _ctx = new FormatEvalContext(_col);
+ reload();
+ }
+
+ /**
+ * Returns the currently loaded "Format" property for this formatter, may be
+ * {@code null}.
+ */
+ public String getFormatString() {
+ return _fmtStr;
+ }
+
+ /**
+ * Sets the given format string as the "Format" property for the underlying
+ * Column and reloads this formatter.
+ *
+ * @param fmtStr the new format string. may be {@code null}, in which case
+ * the "Format" property is removed from the underlying Column
+ */
+ public void setFormatString(String fmtStr) throws IOException {
+ PropertyMap props = _col.getProperties();
+ if(!StringUtils.isEmpty(fmtStr)) {
+ props.put(PropertyMap.FORMAT_PROP, fmtStr);
+ } else {
+ props.remove(PropertyMap.FORMAT_PROP);
+ }
+ props.save();
+ reload();
+ }
+
+ /**
+ * Formats the given value according to the format currently defined for the
+ * underlying Column.
+ *
+ * @param val a valid input value for the DataType of the underlying Column
+ * (i.e. a value which could be passed to a Table add/update
+ * method for this Column). may be {@code null}
+ *
+ * @return the formatted result, always non-{@code null}
+ */
+ public String format(Object val) {
+ return _ctx.format(val);
+ }
+
+ /**
+ * Convenience method for retrieving the appropriate Column value from the
+ * given row array and formatting it.
+ *
+ * @return the formatted result, always non-{@code null}
+ */
+ public String getRowValue(Object[] rowArray) {
+ return format(_col.getRowValue(rowArray));
+ }
+
+ /**
+ * Convenience method for retrieving the appropriate Column value from the
+ * given row map and formatting it.
+ *
+ * @return the formatted result, always non-{@code null}
+ */
+ public String getRowValue(Map<String,?> rowMap) {
+ return format(_col.getRowValue(rowMap));
+ }
+
+ /**
+ * If the properties for the underlying Column have been modified directly
+ * (or the EvalConfig for the underlying Database has been modified), this
+ * method may be called to reload the format for the underlying Column.
+ */
+ public final void reload() throws IOException {
+ _fmt = null;
+ _fmtStr = null;
+
+ _fmtStr = (String)_col.getProperties().getValue(PropertyMap.FORMAT_PROP);
+ _fmt = FormatUtil.createStandaloneFormatter(_ctx, _fmtStr, 1, 1);
+ }
+
+ /**
+ * Utility class to provide an EvalContext for the expression evaluation
+ * engine format support.
+ */
+ private class FormatEvalContext extends ColEvalContext
+ {
+ private FormatEvalContext(ColumnImpl col) {
+ super(col);
+ }
+
+ public String format(Object val) {
+ try {
+ return _fmt.format(toValue(val)).getAsString(this);
+ } catch(EvalException ee) {
+ // invalid values for a given format result in returning the value as is
+ return val.toString();
+ }
+ }
+ }
+}
*
* @author James Ahlborn
*/
-public interface ColumnValidator
+public interface ColumnValidator
{
/**
* Validates and/or manipulates the given potential new value for the given
public static Database create(FileFormat fileFormat, boolean keep)
throws Exception
{
- return create(fileFormat, keep, false);
+ return create(fileFormat, keep, true);
}
public static Database createMem(FileFormat fileFormat) throws Exception {
- return create(fileFormat, false, true);
+ return create(fileFormat);
+ }
+
+ public static Database createFile(FileFormat fileFormat) throws Exception {
+ return create(fileFormat, false, false);
}
private static Database create(FileFormat fileFormat, boolean keep,
boolean inMem)
throws Exception
{
- FileChannel channel = (inMem ? MemFileChannel.newChannel() : null);
+
+ FileChannel channel = ((inMem && !keep) ? MemFileChannel.newChannel() :
+ null);
if (fileFormat == FileFormat.GENERIC_JET4) {
// while we don't support creating GENERIC_JET4 as a jackcess feature,
private static void doTestCodecHandler(boolean simple) throws Exception
{
for(Database.FileFormat ff : SUPPORTED_FILEFORMATS) {
- Database db = TestUtil.create(ff);
+ Database db = TestUtil.createFile(ff);
int pageSize = ((DatabaseImpl)db).getFormat().PAGE_SIZE;
File dbFile = db.getFile();
db.close();
assertEquals(valuePrefix.length() + 100, value.length());
}
- private static void encodeFile(File dbFile, int pageSize, boolean simple)
+ private static void encodeFile(File dbFile, int pageSize, boolean simple)
throws Exception
{
long dbLen = dbFile.length();
bb.clear();
fileChannel.read(bb, offset);
-
+
int pageNumber = (int)(offset / pageSize);
if(simple) {
simpleEncode(bb.array(), bb.array(), pageNumber, 0, pageSize);
}
}
- private static final class SimpleCodecHandler implements CodecHandler
+ private static final class SimpleCodecHandler implements CodecHandler
{
private final TempBufferHolder _bufH = TempBufferHolder.newHolder(
TempBufferHolder.Type.HARD, true);
private final PageChannel _channel;
-
+
private SimpleCodecHandler(PageChannel channel) {
_channel = channel;
}
public boolean canDecodeInline() {
return true;
}
-
+
public void decodePage(ByteBuffer inPage, ByteBuffer outPage,
- int pageNumber)
- throws IOException
+ int pageNumber)
+ throws IOException
{
byte[] arr = inPage.array();
simpleDecode(arr, arr, pageNumber);
}
public ByteBuffer encodePage(ByteBuffer page, int pageNumber,
- int pageOffset)
+ int pageOffset)
throws IOException
{
ByteBuffer bb = _bufH.getPageBuffer(_channel);
bb.clear();
- simpleEncode(page.array(), bb.array(), pageNumber, pageOffset,
+ simpleEncode(page.array(), bb.array(), pageNumber, pageOffset,
page.limit());
return bb;
}
}
- private static final class FullCodecHandler implements CodecHandler
+ private static final class FullCodecHandler implements CodecHandler
{
private final TempBufferHolder _bufH = TempBufferHolder.newHolder(
TempBufferHolder.Type.HARD, true);
private final PageChannel _channel;
-
+
private FullCodecHandler(PageChannel channel) {
_channel = channel;
}
-
+
public boolean canEncodePartialPage() {
return false;
}
public boolean canDecodeInline() {
return true;
}
-
- public void decodePage(ByteBuffer inPage, ByteBuffer outPage,
- int pageNumber)
- throws IOException
+
+ public void decodePage(ByteBuffer inPage, ByteBuffer outPage,
+ int pageNumber)
+ throws IOException
{
byte[] arr = inPage.array();
fullDecode(arr, arr, pageNumber);
}
- public ByteBuffer encodePage(ByteBuffer page, int pageNumber,
- int pageOffset)
+ public ByteBuffer encodePage(ByteBuffer page, int pageNumber,
+ int pageOffset)
throws IOException
{
assertEquals(0, pageOffset);
--- /dev/null
+/*
+Copyright (c) 2019 James Ahlborn
+
+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.healthmarketscience.jackcess.util;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+
+import com.healthmarketscience.jackcess.Column;
+import com.healthmarketscience.jackcess.ColumnBuilder;
+import com.healthmarketscience.jackcess.CursorBuilder;
+import com.healthmarketscience.jackcess.DataType;
+import com.healthmarketscience.jackcess.Database;
+import com.healthmarketscience.jackcess.Database.FileFormat;
+import com.healthmarketscience.jackcess.IndexCursor;
+import com.healthmarketscience.jackcess.PropertyMap;
+import com.healthmarketscience.jackcess.Row;
+import com.healthmarketscience.jackcess.Table;
+import com.healthmarketscience.jackcess.TableBuilder;
+import junit.framework.TestCase;
+import static com.healthmarketscience.jackcess.TestUtil.*;
+import static com.healthmarketscience.jackcess.impl.JetFormatTest.*;
+
+/**
+ *
+ * @author James Ahlborn
+ */
+public class ColumnFormatterTest extends TestCase
+{
+
+ public void testFormat() throws Exception {
+
+ for (final FileFormat fileFormat : SUPPORTED_FILEFORMATS) {
+ Database db = create(fileFormat);
+ db.setEvaluateExpressions(true);
+
+ Table t = new TableBuilder("test")
+ .addColumn(new ColumnBuilder("id", DataType.LONG).setAutoNumber(true))
+ .addColumn(new ColumnBuilder("data1", DataType.TEXT)
+ .putProperty(PropertyMap.FORMAT_PROP,
+ ">@@\\x\\x"))
+ .addColumn(new ColumnBuilder("data2", DataType.LONG)
+ .putProperty(PropertyMap.FORMAT_PROP,
+ "#.#E+0"))
+ .addColumn(new ColumnBuilder("data3", DataType.MONEY)
+ .putProperty(PropertyMap.FORMAT_PROP,
+ "Currency"))
+ .toTable(db);
+
+ ColumnFormatter d1Fmt = new ColumnFormatter(t.getColumn("data1"));
+ ColumnFormatter d2Fmt = new ColumnFormatter(t.getColumn("data2"));
+ ColumnFormatter d3Fmt = new ColumnFormatter(t.getColumn("data3"));
+
+ t.addRow(Column.AUTO_NUMBER, "foobar", 37, "0.03");
+ t.addRow(Column.AUTO_NUMBER, "37", 4500, 4500);
+ t.addRow(Column.AUTO_NUMBER, "foobarbaz", -37, "-37.13");
+ t.addRow(Column.AUTO_NUMBER, null, null, null);
+
+ List<String> found = new ArrayList<>();
+ for(Row r : t) {
+ found.add(d1Fmt.getRowValue(r));
+ found.add(d2Fmt.getRowValue(r));
+ found.add(d3Fmt.getRowValue(r));
+ }
+
+ assertEquals(Arrays.asList(
+ "FOxxOBAR", "3.7E+1", "$0.03",
+ "37xx", "4.5E+3", "$4,500.00",
+ "FOxxOBARBAZ", "-3.7E+1", "($37.13)",
+ "", "", ""),
+ found);
+
+ d1Fmt.setFormatString("Scientific");
+ d2Fmt.setFormatString(null);
+ d3Fmt.setFormatString("General Date");
+
+ assertEquals("Scientific", t.getColumn("data1").getProperties()
+ .getValue(PropertyMap.FORMAT_PROP));
+ assertEquals("General Date", t.getColumn("data3").getProperties()
+ .getValue(PropertyMap.FORMAT_PROP));
+
+ found = new ArrayList<>();
+ for(Row r : t) {
+ found.add(d1Fmt.getRowValue(r));
+ found.add(d2Fmt.getRowValue(r));
+ found.add(d3Fmt.getRowValue(r));
+ }
+
+ assertEquals(Arrays.asList(
+ "foobar", "37", "12:43:12 AM",
+ "3.70E+1", "4500", "4/26/1912",
+ "foobarbaz", "-37", "11/23/1899 3:07:12 AM",
+ "", "", ""),
+ found);
+
+ db.close();
+ }
+ }
+}