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