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