View Javadoc
1   /*
2   Copyright (c) 2008 Health Market Science, Inc.
3   
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7   
8       http://www.apache.org/licenses/LICENSE-2.0
9   
10  Unless required by applicable law or agreed to in writing, software
11  distributed under the License is distributed on an "AS IS" BASIS,
12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  See the License for the specific language governing permissions and
14  limitations under the License.
15  */
16  
17  package com.healthmarketscience.jackcess;
18  
19  import java.io.IOException;
20  import java.util.HashMap;
21  import java.util.Map;
22  
23  import com.healthmarketscience.jackcess.complex.ComplexDataType;
24  import com.healthmarketscience.jackcess.impl.ColumnImpl;
25  import com.healthmarketscience.jackcess.impl.ComplexColumnDesc;
26  import com.healthmarketscience.jackcess.impl.DatabaseImpl;
27  import com.healthmarketscience.jackcess.impl.JetFormat;
28  import com.healthmarketscience.jackcess.impl.PropertyMapImpl;
29  import com.healthmarketscience.jackcess.impl.TableImpl;
30  import com.healthmarketscience.jackcess.impl.TableUpdater;
31  import com.healthmarketscience.jackcess.impl.ToStringBuilder;
32  
33  /**
34   * Builder style class for constructing a {@link Column}.  See {@link
35   * TableBuilder} for example usage.  Additionally, a Column can be added to an
36   * existing Table using the {@link #addToTable(Table)} method.
37   *
38   * @author James Ahlborn
39   * @see TableBuilder
40   * @usage _general_class_
41   */
42  public class ColumnBuilder {
43  
44    /** marks a column id which has not been assigned yet */
45    private static final short UNSET_COLUMN_ID = (short)-1;
46  
47    /** name of the new column */
48    private String _name;
49    /** the type of the new column */
50    private DataType _type;
51    /** optional length for the new column */
52    private Short _length;
53    /** optional precision for the new column */
54    private Byte _precision;
55    /** optional scale for the new column */
56    private Byte _scale;
57    /** whether or not the column is auto-number */
58    private boolean _autoNumber;
59    /** whether or not the column allows compressed unicode */
60    private boolean _compressedUnicode;
61    /** whether or not the column is calculated */
62    private boolean _calculated;
63    /** whether or not the column is a hyperlink (memo only) */
64    private boolean _hyperlink;
65    /** 0-based column number */
66    private short _columnNumber;
67    /** id for the new column, or {@link #UNSET_COLUMN_ID} to use the column
68        number */
69    private short _columnId = UNSET_COLUMN_ID;
70    /** whether or not the column keeps a version history (memo only) */
71    private boolean _appendOnly;
72    /** the complex column this builder declares, if any */
73    private ComplexColumnDesc _complexDesc;
74    /** the collating sort order for a text field */
75    private ColumnImpl.SortOrder _sortOrder;
76    /** table properties (if any) */
77    private Map<String,PropertyMap.Property> _props;
78  
79  
80    public ColumnBuilder(String name) {
81      this(name, null);
82    }
83  
84    public ColumnBuilder(String name, DataType type) {
85      _name = name;
86      _type = type;
87    }
88  
89    public String getName() {
90      return _name;
91    }
92  
93    /**
94     * Sets the type for the new column.
95     */
96    public ColumnBuilder setType(DataType type) {
97      _type = type;
98      return this;
99    }
100 
101   public DataType getType() {
102     return _type;
103   }
104 
105   /**
106    * Sets the type for the new column based on the given SQL type.
107    */
108   public ColumnBuilder setSQLType(int type) throws IOException {
109     return setSQLType(type, 0, null);
110   }
111 
112   /**
113    * Sets the type for the new column based on the given SQL type and target
114    * data length (in type specific units).
115    */
116   public ColumnBuilder setSQLType(int type, int lengthInUnits)
117     throws IOException
118   {
119     return setSQLType(type, lengthInUnits, null);
120   }
121 
122   /**
123    * Sets the type for the new column based on the given SQL type, target
124    * data length (in type specific units), and target FileFormat.
125    */
126   public ColumnBuilder setSQLType(int type, int lengthInUnits,
127                                   Database.FileFormat fileFormat)
128     throws IOException
129   {
130     return setType(DataType.fromSQLType(type, lengthInUnits, fileFormat));
131   }
132 
133   /**
134    * Sets the precision for the new column.
135    */
136   public ColumnBuilder setPrecision(int newPrecision) {
137     _precision = (byte)newPrecision;
138     return this;
139   }
140 
141   public byte getPrecision() {
142     return ((_precision != null) ? _precision : (byte)_type.getDefaultPrecision());
143   }
144 
145   /**
146    * Sets the precision for the new column to the max length for the type.
147    * Does nothing for types which do not have a precision.
148    */
149   public ColumnBuilder setMaxPrecision() {
150     if(_type.getHasScalePrecision()) {
151       setPrecision(_type.getMaxPrecision());
152     }
153     return this;
154   }
155 
156   /**
157    * Sets the scale for the new column.
158    */
159   public ColumnBuilder setScale(int newScale) {
160     _scale = (byte)newScale;
161     return this;
162   }
163 
164   public byte getScale() {
165     return ((_scale != null) ? _scale : (byte)_type.getDefaultScale());
166   }
167 
168   /**
169    * Sets the scale for the new column to the max length for the type.  Does
170    * nothing for types which do not have a scale.
171    */
172   public ColumnBuilder setMaxScale() {
173     if(_type.getHasScalePrecision()) {
174       setScale(_type.getMaxScale());
175     }
176     return this;
177   }
178 
179   /**
180    * Sets the length (in bytes) for the new column.
181    */
182   public ColumnBuilder setLength(int length) {
183     _length = (short)length;
184     return this;
185   }
186 
187   public short getLength() {
188     return ((_length != null) ? _length :
189             (short)(!_type.isVariableLength() ? _type.getFixedSize() :
190                     _type.getDefaultSize()));
191   }
192 
193   /**
194    * Sets the length (in type specific units) for the new column.
195    */
196   public ColumnBuilder setLengthInUnits(int unitLength) {
197     return setLength(_type.fromUnitSize(unitLength));
198   }
199 
200   /**
201    * Sets the length for the new column to the max length for the type.  Does
202    * nothing for types which are not variable length.
203    */
204   public ColumnBuilder setMaxLength() {
205     // length setting only makes sense for variable length columns
206     if(_type.isVariableLength()) {
207       setLength(_type.getMaxSize());
208     }
209     return this;
210   }
211 
212   /**
213    * Sets whether of not the new column is an auto-number column.
214    */
215   public ColumnBuilder setAutoNumber(boolean autoNumber) {
216     _autoNumber = autoNumber;
217     return this;
218   }
219 
220   public boolean isAutoNumber() {
221     return _autoNumber;
222   }
223 
224   /**
225    * Sets whether of not the new column allows unicode compression.
226    */
227   public ColumnBuilder setCompressedUnicode(boolean compressedUnicode) {
228     _compressedUnicode = compressedUnicode;
229     return this;
230   }
231 
232   public boolean isCompressedUnicode() {
233     return _compressedUnicode;
234   }
235 
236   /**
237    * Sets whether of not the new column is a calculated column.
238    */
239   public ColumnBuilder setCalculated(boolean calculated) {
240     _calculated = calculated;
241     return this;
242   }
243 
244   public boolean isCalculated() {
245     return _calculated;
246   }
247 
248   /**
249    * Convenience method to set the various info for a calculated type (flag,
250    * result type property and expression)
251    */
252   public ColumnBuilder setCalculatedInfo(String expression) {
253     setCalculated(true);
254     putProperty(PropertyMap.EXPRESSION_PROP, expression);
255     return putProperty(PropertyMap.RESULT_TYPE_PROP, getType().getValue());
256   }
257 
258   /**
259    * Makes the new column a multi-value complex column which holds values of
260    * the given type, one of BYTE, INT, LONG, FLOAT, DOUBLE, GUID, NUMERIC or
261    * TEXT.  The values live in a supporting table whose shape comes from the
262    * matching {@code MSysComplexType_} table of the database, so the length
263    * and the precision of a value are not the caller's to choose.
264    * <p>
265    * A multi-value column also needs a row source, from either {@link
266    * #setValueList} or {@link #setRowSource}.
267    */
268   public ColumnBuilder setMultiValue(DataType valueType) {
269     setType(DataType.COMPLEX_TYPE);
270     setAutoNumber(true);
271     _complexDesc = ComplexColumnDesc.multiValue(valueType);
272     return this;
273   }
274 
275   /**
276    * Gives a multi-value column the list of values which ms access offers.
277    * At least one value is necessary, because ms access offers no way to add
278    * a value which its row source does not name.
279    */
280   public ColumnBuilder setValueList(String... values) {
281     StringBuilder rowSource = new StringBuilder();
282     for(String value : values) {
283       if(rowSource.length() > 0) {
284         rowSource.append(';');
285       }
286       rowSource.append('"').append(value.replace("\"", "\"\"")).append('"');
287     }
288     getMultiValueDesc().setRowSource("Value List", rowSource.toString(), 1);
289     return this;
290   }
291 
292   /**
293    * Gives a multi-value column a table or a query as the source of the values
294    * which ms access offers.
295    *
296    * @param tableOrQuery the name of a table or a query, or a SELECT statement
297    * @param columnCount the number of columns it gives
298    */
299   public ColumnBuilder setRowSource(String tableOrQuery, int columnCount) {
300     getMultiValueDesc().setRowSource("Table/Query", tableOrQuery, columnCount);
301     return this;
302   }
303 
304   /**
305    * Makes the new column an attachment column.  The shape of an attachment
306    * comes from the {@code MSysComplexType_Attachment} table of the database,
307    * so an attachment column takes no further settings.
308    */
309   public ColumnBuilder setAttachment() {
310     setType(DataType.COMPLEX_TYPE);
311     setAutoNumber(true);
312     _complexDesc = ComplexColumnDesc.attachment();
313     return this;
314   }
315 
316   /**
317    * Sets whether or not a memo column keeps the history of its values.  The
318    * versions live in a complex column which jackcess adds beside the memo
319    * column, and which is read back through {@link
320    * Column#getComplexInfo()} of that added column.
321    */
322   public ColumnBuilder setAppendOnly(boolean appendOnly) {
323     _appendOnly = appendOnly;
324     if(appendOnly) {
325       putProperty(PropertyMap.APPEND_ONLY_PROP, DataType.BOOLEAN, Boolean.TRUE);
326     } else if(_props != null) {
327       // the property tells ms access the column keeps a history, so it cannot
328       // outlive the column which holds one
329       _props.remove(PropertyMap.APPEND_ONLY_PROP);
330     }
331     return this;
332   }
333 
334   public boolean isAppendOnly() {
335     return _appendOnly;
336   }
337 
338   private ComplexColumnDesc getMultiValueDesc() {
339     if((_complexDesc == null) ||
340        (_complexDesc.getType() != ComplexDataType.MULTI_VALUE)) {
341       throw new IllegalArgumentException(withErrorContext(
342           "Only a multi-value column has a row source, and setMultiValue " +
343           "declares one"));
344     }
345     return _complexDesc;
346   }
347 
348   /**
349    * @return the complex column this builder declares, or {@code null} if it
350    *         declares none.  Read through this rather than the field, because
351    *         jackcess builds a complex column of its own which overrides it.
352    * @usage _advanced_method_
353    */
354   public ComplexColumnDesc getComplexDesc() {
355     return _complexDesc;
356   }
357 
358   public boolean isVariableLength() {
359     // calculated columns are written as var len
360     return(getType().isVariableLength() || isCalculated());
361   }
362 
363   /**
364    * Sets whether of not the new column allows unicode compression.
365    */
366   public ColumnBuilder setHyperlink(boolean hyperlink) {
367     _hyperlink = hyperlink;
368     return this;
369   }
370 
371   public boolean isHyperlink() {
372     return _hyperlink;
373   }
374 
375   /**
376    * Sets the column property with the given name to the given value.  Attempts
377    * to determine the type of the property (see
378    * {@link PropertyMap#put(String,Object)} for details on determining the
379    * property type).
380    */
381   public ColumnBuilder putProperty(String name, Object value) {
382     return putProperty(name, null, value);
383   }
384 
385   /**
386    * Sets the column property with the given name and type to the given value.
387    */
388   public ColumnBuilder putProperty(String name, DataType type, Object value) {
389     setProperty(name, PropertyMapImpl.createProperty(name, type, value));
390     return this;
391   }
392 
393   public Map<String,PropertyMap.Property> getProperties() {
394     return _props;
395   }
396 
397   private void setProperty(String name, PropertyMap.Property prop) {
398     if(prop == null) {
399       return;
400     }
401     if(_props == null) {
402       _props = new HashMap<>();
403     }
404     _props.put(name, prop);
405   }
406 
407   private PropertyMap.Property getProperty(String name) {
408     return ((_props != null) ? _props.get(name) : null);
409   }
410 
411   /**
412    * Sets all attributes except name from the given Column template (including
413    * all column properties except GUID).
414    */
415   public ColumnBuilder setFromColumn(Column template)
416     throws IOException
417   {
418     DataType type = template.getType();
419     setType(type);
420     setLengthInUnits(template.getLengthInUnits());
421     setAutoNumber(template.isAutoNumber());
422     if(type.getHasScalePrecision()) {
423       setScale(template.getScale());
424       setPrecision(template.getPrecision());
425     }
426     setCalculated(template.isCalculated());
427     setCompressedUnicode(template.isCompressedUnicode());
428     setHyperlink(template.isHyperlink());
429     setAppendOnly(template.isAppendOnly());
430     _complexDesc = ComplexColumnDesc.fromColumn(template);
431     if(template instanceof ColumnImpl) {
432       setTextSortOrder(((ColumnImpl)template).getTextSortOrder());
433     }
434 
435     PropertyMap colProps = template.getProperties();
436     for(PropertyMap.Property colProp : colProps) {
437       // copy everything but guid
438       if(!PropertyMap.GUID_PROP.equalsIgnoreCase(colProp.getName())) {
439         setProperty(colProp.getName(), colProp);
440       }
441     }
442 
443     return this;
444   }
445 
446   /**
447    * Sets all attributes except name from the given Column template.
448    */
449   public ColumnBuilderm/healthmarketscience/jackcess/ColumnBuilder.html#ColumnBuilder">ColumnBuilder setFromColumn(ColumnBuilder template) {
450     DataType type = template.getType();
451     _type = type;
452     _length = template._length;
453     _autoNumber = template._autoNumber;
454     if(type.getHasScalePrecision()) {
455       _scale = template._scale;
456       _precision = template._precision;
457     }
458     _calculated = template._calculated;
459     _compressedUnicode = template._compressedUnicode;
460     _hyperlink = template._hyperlink;
461     _appendOnly = template._appendOnly;
462     // a copy of the declaration, because each complex column owns the id of
463     // its own MSysComplexColumns row
464     ComplexColumnDesc templateDesc = template.getComplexDesc();
465     _complexDesc = ((templateDesc != null) ? templateDesc.copy() : null);
466     _sortOrder = template._sortOrder;
467 
468     if(template._props != null) {
469       _props = new HashMap<>(template._props);
470     }
471 
472     return this;
473   }
474 
475   /**
476    * Escapes the new column's name using {@link TableBuilder#escapeIdentifier}.
477    */
478   public ColumnBuilder escapeName() {
479     _name = TableBuilder.escapeIdentifier(_name);
480     return this;
481   }
482 
483   /**
484    * @usage _advanced_method_
485    */
486   public short getColumnNumber() {
487     return _columnNumber;
488   }
489 
490   /**
491    * @usage _advanced_method_
492    */
493   public void setColumnNumber(short newColumnNumber) {
494     _columnNumber = newColumnNumber;
495   }
496 
497   /**
498    * @usage _advanced_method_
499    */
500   public short getColumnId() {
501     return ((_columnId != UNSET_COLUMN_ID) ? _columnId : _columnNumber);
502   }
503 
504   /**
505    * @usage _advanced_method_
506    */
507   public void setColumnId(short newColumnId) {
508     _columnId = newColumnId;
509   }
510 
511   /**
512    * @usage _advanced_method_
513    */
514   public ColumnImpl.SortOrder getTextSortOrder() {
515     return _sortOrder;
516   }
517 
518   /**
519    * @usage _advanced_method_
520    */
521   public void setTextSortOrder(ColumnImpl.SortOrder newTextSortOrder) {
522     _sortOrder = newTextSortOrder;
523   }
524 
525   /**
526    * @usage _advanced_method_
527    */
528   public boolean storeInNullMask() {
529     return (getType() == DataType.BOOLEAN);
530   }
531 
532   /**
533    * @usage _advanced_method_
534    */
535   public int getFixedDataSize() {
536     return _type.getFixedSize(_length);
537   }
538 
539   /**
540    * Checks that this column definition is valid.
541    *
542    * @throws IllegalArgumentException if this column definition is invalid.
543    * @usage _advanced_method_
544    */
545   public void validate(JetFormat format) {
546     DatabaseImpl.validateIdentifierName(
547         getName(), format.MAX_COLUMN_NAME_LENGTH, "column");
548 
549     if(getType() == null) {
550       throw new IllegalArgumentException(withErrorContext("must have type"));
551     }
552     if(getType().isUnsupported()) {
553       throw new IllegalArgumentException(withErrorContext(
554           "Cannot create column with unsupported type " + getType()));
555     }
556     if(!format.isSupportedDataType(getType())) {
557       throw new IllegalArgumentException(withErrorContext(
558           "Database format " + format + " does not support type " + getType()));
559     }
560 
561     if(!getType().isVariableLength()) {
562       if(getLength() < getType().getFixedSize()) {
563         throw new IllegalArgumentException(withErrorContext(
564             "Invalid fixed length size " + getLength()));
565       }
566     } else if(!getType().isLongValue()) {
567       if(!getType().isValidSize(getLength())) {
568         throw new IllegalArgumentException(withErrorContext(
569             "Var length must be from " + getType().getMinSize() + " to " +
570             getType().getMaxSize() + " inclusive, found " + getLength()));
571       }
572     }
573 
574     if(getType().getHasScalePrecision()) {
575       if(!getType().isValidScale(getScale())) {
576         throw new IllegalArgumentException(withErrorContext(
577             "Scale must be from " + getType().getMinScale() + " to " +
578             getType().getMaxScale() + " inclusive, found " + getScale()));
579       }
580       if(!getType().isValidPrecision(getPrecision())) {
581         throw new IllegalArgumentException(withErrorContext(
582             "Precision must be from " + getType().getMinPrecision() + " to " +
583             getType().getMaxPrecision() + " inclusive, found " +
584             getPrecision()));
585       }
586     }
587 
588     if(isAutoNumber()) {
589       if(!getType().mayBeAutoNumber()) {
590         throw new IllegalArgumentException(withErrorContext(
591             "Auto number column must be long integer or guid"));
592       }
593     }
594 
595     if(isCompressedUnicode()) {
596       if(!getType().isTextual()) {
597         throw new IllegalArgumentException(withErrorContext(
598             "Only textual columns allow unicode compression (text/memo)"));
599       }
600     }
601 
602     if(isHyperlink()) {
603       if(getType() != DataType.MEMO) {
604         throw new IllegalArgumentException(withErrorContext(
605             "Only memo columns can be hyperlinks"));
606       }
607     }
608 
609     if(isAppendOnly()) {
610       if(getType() != DataType.MEMO) {
611         throw new IllegalArgumentException(withErrorContext(
612             "Only memo columns can keep a version history"));
613       }
614     }
615 
616     if(getType() == DataType.COMPLEX_TYPE) {
617       ComplexColumnDesc complexDesc = getComplexDesc();
618       if(complexDesc == null) {
619         throw new IllegalArgumentException(withErrorContext(
620             "A complex column is declared with setMultiValue, setAttachment " +
621             "or, for a version history, setAppendOnly on a memo column"));
622       }
623       complexDesc.validate(getName());
624     }
625 
626     if(isCalculated()) {
627       if(!format.isSupportedCalculatedDataType(getType())) {
628         throw new IllegalArgumentException(withErrorContext(
629             "Database format " + format + " does not support calculated type " +
630             getType()));
631       }
632 
633       // must have an expression
634       if(getProperty(PropertyMap.EXPRESSION_PROP) == null) {
635         throw new IllegalArgumentException(withErrorContext(
636             "No expression provided for calculated type " + getType()));
637       }
638 
639       // must have result type (just fill in if missing)
640       if(getProperty(PropertyMap.RESULT_TYPE_PROP) == null) {
641         putProperty(PropertyMap.RESULT_TYPE_PROP, getType().getValue());
642       }
643     }
644   }
645 
646   /**
647    * Creates a new Column with the currently configured attributes.
648    */
649   public ColumnBuilder toColumn() {
650     // for backwards compat w/ old code
651     return this;
652   }
653 
654   /**
655    * Adds a new Column to the given Table with the currently configured
656    * attributes.
657    */
658   public Column addToTable(Table table) throws IOException {
659     return addToTableDefinition(table);
660   }
661 
662   /**
663    * Adds a new Column to the given TableDefinition with the currently
664    * configured attributes.
665    */
666   public Column addToTableDefinition(TableDefinition table) throws IOException {
667       return new TableUpdater((TableImpl)table).addColumn(this);
668   }
669 
670   @Override
671   public String toString() {
672     return ToStringBuilder.builder(this)
673       .append("name", _name)
674       .append("type", _type)
675       .append("number", _columnNumber)
676       .append("id", getColumnId())
677       .appendIfNotNull("length", _length)
678       .appendIfNotNull("precision", _precision)
679       .appendIfNotNull("scale", _scale)
680       .append("autoNumber", _autoNumber)
681       .append("compressedUnicode", _compressedUnicode)
682       .append("calculated", _calculated)
683       .append("hyperlink", _hyperlink)
684       .append("appendOnly", _appendOnly)
685       .appendIfNotNull("complex", getComplexDesc())
686       .appendIfNotNull("textSortOrder", _sortOrder)
687       .appendIfNotNull("props", _props)
688       .toString();
689   }
690 
691   private String withErrorContext(String msg) {
692     return msg + "(Column=" + getName() + ")";
693   }
694 }