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.impl.ColumnImpl;
24  import com.healthmarketscience.jackcess.impl.DatabaseImpl;
25  import com.healthmarketscience.jackcess.impl.JetFormat;
26  import com.healthmarketscience.jackcess.impl.PropertyMapImpl;
27  import com.healthmarketscience.jackcess.impl.TableImpl;
28  import com.healthmarketscience.jackcess.impl.TableUpdater;
29  import com.healthmarketscience.jackcess.impl.ToStringBuilder;
30  
31  /**
32   * Builder style class for constructing a {@link Column}.  See {@link
33   * TableBuilder} for example usage.  Additionally, a Column can be added to an
34   * existing Table using the {@link #addToTable(Table)} method.
35   *
36   * @author James Ahlborn
37   * @see TableBuilder
38   * @usage _general_class_
39   */
40  public class ColumnBuilder {
41  
42    /** marks a column id which has not been assigned yet */
43    private static final short UNSET_COLUMN_ID = (short)-1;
44  
45    /** name of the new column */
46    private String _name;
47    /** the type of the new column */
48    private DataType _type;
49    /** optional length for the new column */
50    private Short _length;
51    /** optional precision for the new column */
52    private Byte _precision;
53    /** optional scale for the new column */
54    private Byte _scale;
55    /** whether or not the column is auto-number */
56    private boolean _autoNumber;
57    /** whether or not the column allows compressed unicode */
58    private boolean _compressedUnicode;
59    /** whether or not the column is calculated */
60    private boolean _calculated;
61    /** whether or not the column is a hyperlink (memo only) */
62    private boolean _hyperlink;
63    /** 0-based column number */
64    private short _columnNumber;
65    /** id for the new column, or {@link #UNSET_COLUMN_ID} to use the column
66        number */
67    private short _columnId = UNSET_COLUMN_ID;
68    /** the collating sort order for a text field */
69    private ColumnImpl.SortOrder _sortOrder;
70    /** table properties (if any) */
71    private Map<String,PropertyMap.Property> _props;
72  
73  
74    public ColumnBuilder(String name) {
75      this(name, null);
76    }
77  
78    public ColumnBuilder(String name, DataType type) {
79      _name = name;
80      _type = type;
81    }
82  
83    public String getName() {
84      return _name;
85    }
86  
87    /**
88     * Sets the type for the new column.
89     */
90    public ColumnBuilder setType(DataType type) {
91      _type = type;
92      return this;
93    }
94  
95    public DataType getType() {
96      return _type;
97    }
98  
99    /**
100    * Sets the type for the new column based on the given SQL type.
101    */
102   public ColumnBuilder setSQLType(int type) throws IOException {
103     return setSQLType(type, 0, null);
104   }
105 
106   /**
107    * Sets the type for the new column based on the given SQL type and target
108    * data length (in type specific units).
109    */
110   public ColumnBuilder setSQLType(int type, int lengthInUnits)
111     throws IOException
112   {
113     return setSQLType(type, lengthInUnits, null);
114   }
115 
116   /**
117    * Sets the type for the new column based on the given SQL type, target
118    * data length (in type specific units), and target FileFormat.
119    */
120   public ColumnBuilder setSQLType(int type, int lengthInUnits,
121                                   Database.FileFormat fileFormat)
122     throws IOException
123   {
124     return setType(DataType.fromSQLType(type, lengthInUnits, fileFormat));
125   }
126 
127   /**
128    * Sets the precision for the new column.
129    */
130   public ColumnBuilder setPrecision(int newPrecision) {
131     _precision = (byte)newPrecision;
132     return this;
133   }
134 
135   public byte getPrecision() {
136     return ((_precision != null) ? _precision : (byte)_type.getDefaultPrecision());
137   }
138 
139   /**
140    * Sets the precision for the new column to the max length for the type.
141    * Does nothing for types which do not have a precision.
142    */
143   public ColumnBuilder setMaxPrecision() {
144     if(_type.getHasScalePrecision()) {
145       setPrecision(_type.getMaxPrecision());
146     }
147     return this;
148   }
149 
150   /**
151    * Sets the scale for the new column.
152    */
153   public ColumnBuilder setScale(int newScale) {
154     _scale = (byte)newScale;
155     return this;
156   }
157 
158   public byte getScale() {
159     return ((_scale != null) ? _scale : (byte)_type.getDefaultScale());
160   }
161 
162   /**
163    * Sets the scale for the new column to the max length for the type.  Does
164    * nothing for types which do not have a scale.
165    */
166   public ColumnBuilder setMaxScale() {
167     if(_type.getHasScalePrecision()) {
168       setScale(_type.getMaxScale());
169     }
170     return this;
171   }
172 
173   /**
174    * Sets the length (in bytes) for the new column.
175    */
176   public ColumnBuilder setLength(int length) {
177     _length = (short)length;
178     return this;
179   }
180 
181   public short getLength() {
182     return ((_length != null) ? _length :
183             (short)(!_type.isVariableLength() ? _type.getFixedSize() :
184                     _type.getDefaultSize()));
185   }
186 
187   /**
188    * Sets the length (in type specific units) for the new column.
189    */
190   public ColumnBuilder setLengthInUnits(int unitLength) {
191     return setLength(_type.fromUnitSize(unitLength));
192   }
193 
194   /**
195    * Sets the length for the new column to the max length for the type.  Does
196    * nothing for types which are not variable length.
197    */
198   public ColumnBuilder setMaxLength() {
199     // length setting only makes sense for variable length columns
200     if(_type.isVariableLength()) {
201       setLength(_type.getMaxSize());
202     }
203     return this;
204   }
205 
206   /**
207    * Sets whether of not the new column is an auto-number column.
208    */
209   public ColumnBuilder setAutoNumber(boolean autoNumber) {
210     _autoNumber = autoNumber;
211     return this;
212   }
213 
214   public boolean isAutoNumber() {
215     return _autoNumber;
216   }
217 
218   /**
219    * Sets whether of not the new column allows unicode compression.
220    */
221   public ColumnBuilder setCompressedUnicode(boolean compressedUnicode) {
222     _compressedUnicode = compressedUnicode;
223     return this;
224   }
225 
226   public boolean isCompressedUnicode() {
227     return _compressedUnicode;
228   }
229 
230   /**
231    * Sets whether of not the new column is a calculated column.
232    */
233   public ColumnBuilder setCalculated(boolean calculated) {
234     _calculated = calculated;
235     return this;
236   }
237 
238   public boolean isCalculated() {
239     return _calculated;
240   }
241 
242   /**
243    * Convenience method to set the various info for a calculated type (flag,
244    * result type property and expression)
245    */
246   public ColumnBuilder setCalculatedInfo(String expression) {
247     setCalculated(true);
248     putProperty(PropertyMap.EXPRESSION_PROP, expression);
249     return putProperty(PropertyMap.RESULT_TYPE_PROP, getType().getValue());
250   }
251 
252   public boolean isVariableLength() {
253     // calculated columns are written as var len
254     return(getType().isVariableLength() || isCalculated());
255   }
256 
257   /**
258    * Sets whether of not the new column allows unicode compression.
259    */
260   public ColumnBuilder setHyperlink(boolean hyperlink) {
261     _hyperlink = hyperlink;
262     return this;
263   }
264 
265   public boolean isHyperlink() {
266     return _hyperlink;
267   }
268 
269   /**
270    * Sets the column property with the given name to the given value.  Attempts
271    * to determine the type of the property (see
272    * {@link PropertyMap#put(String,Object)} for details on determining the
273    * property type).
274    */
275   public ColumnBuilder putProperty(String name, Object value) {
276     return putProperty(name, null, value);
277   }
278 
279   /**
280    * Sets the column property with the given name and type to the given value.
281    */
282   public ColumnBuilder putProperty(String name, DataType type, Object value) {
283     setProperty(name, PropertyMapImpl.createProperty(name, type, value));
284     return this;
285   }
286 
287   public Map<String,PropertyMap.Property> getProperties() {
288     return _props;
289   }
290 
291   private void setProperty(String name, PropertyMap.Property prop) {
292     if(prop == null) {
293       return;
294     }
295     if(_props == null) {
296       _props = new HashMap<>();
297     }
298     _props.put(name, prop);
299   }
300 
301   private PropertyMap.Property getProperty(String name) {
302     return ((_props != null) ? _props.get(name) : null);
303   }
304 
305   /**
306    * Sets all attributes except name from the given Column template (including
307    * all column properties except GUID).
308    */
309   public ColumnBuilder setFromColumn(Column template)
310     throws IOException
311   {
312     DataType type = template.getType();
313     setType(type);
314     setLengthInUnits(template.getLengthInUnits());
315     setAutoNumber(template.isAutoNumber());
316     if(type.getHasScalePrecision()) {
317       setScale(template.getScale());
318       setPrecision(template.getPrecision());
319     }
320     setCalculated(template.isCalculated());
321     setCompressedUnicode(template.isCompressedUnicode());
322     setHyperlink(template.isHyperlink());
323     if(template instanceof ColumnImpl) {
324       setTextSortOrder(((ColumnImpl)template).getTextSortOrder());
325     }
326 
327     PropertyMap colProps = template.getProperties();
328     for(PropertyMap.Property colProp : colProps) {
329       // copy everything but guid
330       if(!PropertyMap.GUID_PROP.equalsIgnoreCase(colProp.getName())) {
331         setProperty(colProp.getName(), colProp);
332       }
333     }
334 
335     return this;
336   }
337 
338   /**
339    * Sets all attributes except name from the given Column template.
340    */
341   public ColumnBuilderm/healthmarketscience/jackcess/ColumnBuilder.html#ColumnBuilder">ColumnBuilder setFromColumn(ColumnBuilder template) {
342     DataType type = template.getType();
343     _type = type;
344     _length = template._length;
345     _autoNumber = template._autoNumber;
346     if(type.getHasScalePrecision()) {
347       _scale = template._scale;
348       _precision = template._precision;
349     }
350     _calculated = template._calculated;
351     _compressedUnicode = template._compressedUnicode;
352     _hyperlink = template._hyperlink;
353     _sortOrder = template._sortOrder;
354 
355     if(template._props != null) {
356       _props = new HashMap<>(template._props);
357     }
358 
359     return this;
360   }
361 
362   /**
363    * Escapes the new column's name using {@link TableBuilder#escapeIdentifier}.
364    */
365   public ColumnBuilder escapeName() {
366     _name = TableBuilder.escapeIdentifier(_name);
367     return this;
368   }
369 
370   /**
371    * @usage _advanced_method_
372    */
373   public short getColumnNumber() {
374     return _columnNumber;
375   }
376 
377   /**
378    * @usage _advanced_method_
379    */
380   public void setColumnNumber(short newColumnNumber) {
381     _columnNumber = newColumnNumber;
382   }
383 
384   /**
385    * @usage _advanced_method_
386    */
387   public short getColumnId() {
388     return ((_columnId != UNSET_COLUMN_ID) ? _columnId : _columnNumber);
389   }
390 
391   /**
392    * @usage _advanced_method_
393    */
394   public void setColumnId(short newColumnId) {
395     _columnId = newColumnId;
396   }
397 
398   /**
399    * @usage _advanced_method_
400    */
401   public ColumnImpl.SortOrder getTextSortOrder() {
402     return _sortOrder;
403   }
404 
405   /**
406    * @usage _advanced_method_
407    */
408   public void setTextSortOrder(ColumnImpl.SortOrder newTextSortOrder) {
409     _sortOrder = newTextSortOrder;
410   }
411 
412   /**
413    * @usage _advanced_method_
414    */
415   public boolean storeInNullMask() {
416     return (getType() == DataType.BOOLEAN);
417   }
418 
419   /**
420    * @usage _advanced_method_
421    */
422   public int getFixedDataSize() {
423     return _type.getFixedSize(_length);
424   }
425 
426   /**
427    * Checks that this column definition is valid.
428    *
429    * @throws IllegalArgumentException if this column definition is invalid.
430    * @usage _advanced_method_
431    */
432   public void validate(JetFormat format) {
433     DatabaseImpl.validateIdentifierName(
434         getName(), format.MAX_COLUMN_NAME_LENGTH, "column");
435 
436     if(getType() == null) {
437       throw new IllegalArgumentException(withErrorContext("must have type"));
438     }
439     if(getType().isUnsupported()) {
440       throw new IllegalArgumentException(withErrorContext(
441           "Cannot create column with unsupported type " + getType()));
442     }
443     if(!format.isSupportedDataType(getType())) {
444       throw new IllegalArgumentException(withErrorContext(
445           "Database format " + format + " does not support type " + getType()));
446     }
447 
448     if(!getType().isVariableLength()) {
449       if(getLength() < getType().getFixedSize()) {
450         throw new IllegalArgumentException(withErrorContext(
451             "Invalid fixed length size " + getLength()));
452       }
453     } else if(!getType().isLongValue()) {
454       if(!getType().isValidSize(getLength())) {
455         throw new IllegalArgumentException(withErrorContext(
456             "Var length must be from " + getType().getMinSize() + " to " +
457             getType().getMaxSize() + " inclusive, found " + getLength()));
458       }
459     }
460 
461     if(getType().getHasScalePrecision()) {
462       if(!getType().isValidScale(getScale())) {
463         throw new IllegalArgumentException(withErrorContext(
464             "Scale must be from " + getType().getMinScale() + " to " +
465             getType().getMaxScale() + " inclusive, found " + getScale()));
466       }
467       if(!getType().isValidPrecision(getPrecision())) {
468         throw new IllegalArgumentException(withErrorContext(
469             "Precision must be from " + getType().getMinPrecision() + " to " +
470             getType().getMaxPrecision() + " inclusive, found " +
471             getPrecision()));
472       }
473     }
474 
475     if(isAutoNumber()) {
476       if(!getType().mayBeAutoNumber()) {
477         throw new IllegalArgumentException(withErrorContext(
478             "Auto number column must be long integer or guid"));
479       }
480     }
481 
482     if(isCompressedUnicode()) {
483       if(!getType().isTextual()) {
484         throw new IllegalArgumentException(withErrorContext(
485             "Only textual columns allow unicode compression (text/memo)"));
486       }
487     }
488 
489     if(isHyperlink()) {
490       if(getType() != DataType.MEMO) {
491         throw new IllegalArgumentException(withErrorContext(
492             "Only memo columns can be hyperlinks"));
493       }
494     }
495 
496     if(isCalculated()) {
497       if(!format.isSupportedCalculatedDataType(getType())) {
498         throw new IllegalArgumentException(withErrorContext(
499             "Database format " + format + " does not support calculated type " +
500             getType()));
501       }
502 
503       // must have an expression
504       if(getProperty(PropertyMap.EXPRESSION_PROP) == null) {
505         throw new IllegalArgumentException(withErrorContext(
506             "No expression provided for calculated type " + getType()));
507       }
508 
509       // must have result type (just fill in if missing)
510       if(getProperty(PropertyMap.RESULT_TYPE_PROP) == null) {
511         putProperty(PropertyMap.RESULT_TYPE_PROP, getType().getValue());
512       }
513     }
514   }
515 
516   /**
517    * Creates a new Column with the currently configured attributes.
518    */
519   public ColumnBuilder toColumn() {
520     // for backwards compat w/ old code
521     return this;
522   }
523 
524   /**
525    * Adds a new Column to the given Table with the currently configured
526    * attributes.
527    */
528   public Column addToTable(Table table) throws IOException {
529     return addToTableDefinition(table);
530   }
531 
532   /**
533    * Adds a new Column to the given TableDefinition with the currently
534    * configured attributes.
535    */
536   public Column addToTableDefinition(TableDefinition table) throws IOException {
537       return new TableUpdater((TableImpl)table).addColumn(this);
538   }
539 
540   @Override
541   public String toString() {
542     return ToStringBuilder.builder(this)
543       .append("name", _name)
544       .append("type", _type)
545       .append("number", _columnNumber)
546       .append("id", getColumnId())
547       .appendIfNotNull("length", _length)
548       .appendIfNotNull("precision", _precision)
549       .appendIfNotNull("scale", _scale)
550       .append("autoNumber", _autoNumber)
551       .append("compressedUnicode", _compressedUnicode)
552       .append("calculated", _calculated)
553       .append("hyperlink", _hyperlink)
554       .appendIfNotNull("textSortOrder", _sortOrder)
555       .appendIfNotNull("props", _props)
556       .toString();
557   }
558 
559   private String withErrorContext(String msg) {
560     return msg + "(Column=" + getName() + ")";
561   }
562 }