View Javadoc

1   /**
2    * Copyright 2010, CSIRO Australia.
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  /**
18   * 
19   */
20  package au.csiro.netcdf;
21  
22  import java.io.File;
23  import java.io.IOException;
24  import java.io.PrintWriter;
25  import java.io.StringWriter;
26  import java.util.ArrayList;
27  import java.util.List;
28  
29  import org.apache.commons.cli.BasicParser;
30  import org.apache.commons.cli.CommandLine;
31  import org.apache.commons.cli.HelpFormatter;
32  import org.apache.commons.cli.Option;
33  import org.apache.commons.cli.OptionBuilder;
34  import org.apache.commons.cli.Options;
35  import org.apache.commons.cli.ParseException;
36  import org.apache.log4j.Logger;
37  
38  import ucar.nc2.Dimension;
39  import ucar.nc2.NetcdfFileWriteable;
40  import au.csiro.netcdf.cli.Command;
41  import au.csiro.netcdf.cli.CommandLineOptionsComparator;
42  import au.csiro.netcdf.util.Util;
43  
44  /**
45   * The <strong>ncdefineDim</strong> command defines a {@link Dimension} in a netCDF file.
46   * <p>
47   * Copyright 2010, CSIRO Australia 
48   * All rights reserved.
49   * 
50   * @author $Author: robertbridle $ on 17/03/2010
51   * @version $Revision: 84 $ $Date: 2010-08-25 15:56:46 +1000 (Wed, 25 Aug 2010) $ $Id: NcDefineDimension.java 6520
52   *          2010-03-17 06:17:50Z bri26e $
53   */
54  public class NcDefineDimension implements Command
55  {
56      /**
57       * The command name
58       */
59      public static final String NC_DEFINE_DIM_COMMAND_NAME = "ncdefineDim";
60  
61      /**
62       * The name of the command line option used for specifying the output netCDF file name.
63       */
64      public static final String OUTPUT_FILE = "outputFileName";
65  
66      /**
67       * The name of the command line option used for specifying the size of the dimension.
68       */
69      public static final String DIMENSION_SIZE = "dimensionSize";
70  
71      /**
72       * The name of the command line option used for specifying the name of the dimension.
73       */
74      public static final String DIMENSION_NAME = "dimensionName";
75      
76      /**
77       * Whether the netCDF file should be written with large file support, that is, 64-bit addressing for files greater
78       * than 2 GB.
79       */
80      public static final String IS_LARGE_FILE = "largeFileSupport";
81      
82      /**
83       * Constant that defines the logger to be used.
84       */
85      private static final Logger LOG = Logger.getLogger(NcDefineDimension.class.getName());
86  
87      /**
88       * Command options
89       */
90      private Options options = null;
91  
92      /**
93       * Constructor
94       */
95      public NcDefineDimension()
96      {
97          this.options = createOptions();
98      }
99  
100     /*
101      * (non-Javadoc)
102      * 
103      * @see au.csiro.netcdf.Command#execute(java.lang.String[])
104      */
105     @Override
106     public void execute(String[] args) throws ParseException, IOException, IllegalArgumentException
107     {
108         // parse the command line arguments
109         CommandLine parsedCommandLine = new BasicParser().parse(this.options, args);
110 
111         // get the command line argument values
112         String outputFilenameArg = (parsedCommandLine.hasOption(OUTPUT_FILE)) ? parsedCommandLine
113                 .getOptionValue(OUTPUT_FILE) : "";
114         String dimensionSizeArg = (parsedCommandLine.hasOption(DIMENSION_SIZE)) ? parsedCommandLine
115                 .getOptionValue(DIMENSION_SIZE) : "";
116         String dimensionNameArg = (parsedCommandLine.hasOption(DIMENSION_NAME)) ? parsedCommandLine
117                 .getOptionValue(DIMENSION_NAME) : "";
118         boolean isUnlimited = dimensionSizeArg.isEmpty(); // true if empty or not specified, i.e. default value = false.
119         boolean isLargeFileSupport = parsedCommandLine.hasOption(IS_LARGE_FILE);
120 
121         // check whether large file support is needed
122         if (Util.fileExists(outputFilenameArg)
123                 && Util.getExistingFile(outputFilenameArg).length() >= MAX_32BIT_OFFSET_FILE_SIZE
124                 && !isLargeFileSupport)
125         {
126             throw new IllegalArgumentException("The netCDF file will be too large, please use " + IS_LARGE_FILE
127                     + " flag.");
128         }
129         
130         // try getting dimension size, where IllegalArgumentException can be thrown from
131         int dimensionSize = 0; // this is non-mandatory option, its default value is 0.
132         if (!isUnlimited)
133         {
134             dimensionSize = NcDefineDimension.mapStringToDimensionSize(dimensionSizeArg);
135         }
136 
137         this.execute(outputFilenameArg, dimensionNameArg, dimensionSize, isUnlimited, isLargeFileSupport);
138     }
139 
140     /**
141      * Allows the command to be run programmatically, instead of from a command line.
142      * 
143      * @param outputFilename
144      *            the netCDF file in which to define a dimension.
145      * @param dimensionName
146      *            the name of the dimension, this value will be used to reference the dimension.
147      * @param dimensionSize
148      *            the size of the dimension, if the dimension is unlimited, then a size of 0 is used.
149      * @param isUnlimited
150      *            whether the size of the dimension is unlimited. note: only the first dimension defined can be
151      *            unlimited.
152      * @param isLargeFileSupport
153      *            whether the netCDF file should be written with large file support, i.e. 64-bit addressing for files
154      *            greater than 2 GB.
155      * @throws IOException
156      *             thrown if netCDF can to be written to or read from.
157      * @throws SecurityException
158      *             thrown if a security manager exists and it prevents the netCDF file from being created.
159      * @throws IllegalStateException
160      *             thrown if the {@link Dimension} can not be added to the netCDF-3 file.
161      */
162     public void execute(String outputFilename, String dimensionName, int dimensionSize, boolean isUnlimited,
163             boolean isLargeFileSupport) throws IOException, SecurityException, IllegalStateException
164     {
165         // the netcdf file to be written.
166         NetcdfFileWriteable ncfile = null;
167 
168         // does output file exist
169         File outputFile = new File(outputFilename);
170         if (outputFile.exists())
171         {
172             ncfile = NetcdfFileWriteable.openExisting(outputFilename, false /*
173                                                                             * Setting fill = true causes everything to
174                                                                             * be written twice: first with the fill
175                                                                             * value, then with the data values. If you
176                                                                             * know you will write all the data, you dont
177                                                                             * need to use fill. If you don't know if all
178                                                                             * the data will be written, turning fill on
179                                                                             * ensures that any values not written will
180                                                                             * have the fill value. Otherwise those
181                                                                             * values will be undefined: possibly zero,
182                                                                             * or possibly garbage.
183                                                                             */);
184             try
185             {                
186                 // allow editing header info
187                 ncfile.setRedefineMode(true);
188 
189                 Dimension dimension = new Dimension(dimensionName, dimensionSize/*
190                                                                                  * number of data points
191                                                                                  */, true /* isShared */,
192                         isUnlimited /* isUnlimited */, false /* isVariableLength */);
193                 
194                 // check the dimension to be added
195                 this.checkDimensionLimitiations(ncfile, dimension);
196                 
197                 // add dimension
198                 ncfile.addDimension(null, dimension);
199 
200                 // close editing header info
201                 ncfile.setRedefineMode(false);
202             }
203             finally
204             {
205                 if(ncfile!=null)
206                 {
207                     try
208                     {
209                         ncfile.close();
210                     }
211                     catch(Exception e)
212                     {
213                         LOG.error("Could not close file: " + outputFilename, e);
214                     }
215                 }
216             }
217         }
218         else
219         {
220             File parentFile = outputFile.getParentFile();
221             if (parentFile != null)
222             {
223                 parentFile.mkdirs(); // create the file's parent folder structure
224             }
225             ncfile = NetcdfFileWriteable.createNew(outputFilename, false /*
226                                                                          * Setting fill = true causes everything to be
227                                                                          * written twice: first with the fill value,
228                                                                          * then with the data values. If you know you
229                                                                          * will write all the data, you dont need to use
230                                                                          * fill. If you don't know if all the data will
231                                                                          * be written, turning fill on ensures that any
232                                                                          * values not written will have the fill value.
233                                                                          * Otherwise those values will be undefined:
234                                                                          * possibly zero, or possibly garbage.
235                                                                          */);
236             try
237             {
238                 Dimension dimension = new Dimension(dimensionName, Integer.valueOf(dimensionSize).intValue()/*
239                                                                                                              * number of
240                                                                                                              * data
241                                                                                                              * points
242                                                                                                              */,
243                         true /* isShared */, isUnlimited /* isUnlimited */, false /* isVariableLength */);
244 
245                 // check the dimension to be added
246                 this.checkDimensionLimitiations(ncfile, dimension);
247                 
248                 // add dimension
249                 ncfile.addDimension(null, dimension);
250 
251                 // when all objects are added, call create(). You cannot read or write data until create() is called.
252                 ncfile.create();
253             }
254             finally
255             {
256                 if(ncfile!=null)
257                 {
258                     try
259                     {
260                         ncfile.close();
261                     }
262                     catch(Exception e)
263                     {
264                         LOG.error("Could not close file: " + outputFilename, e);
265                     }
266                 }
267             }
268         }
269     }
270 
271     /*
272      * (non-Javadoc)
273      * 
274      * @see au.csiro.netcdf.Command#getCommandName()
275      */
276     @Override
277     public String getCommandName()
278     {
279         return NcDefineDimension.NC_DEFINE_DIM_COMMAND_NAME;
280     }
281 
282     /*
283      * (non-Javadoc)
284      * 
285      * @see java.lang.Object#toString()
286      */
287     public String toString()
288     {
289         // generate the help/usage statement
290         String header = "Define a dimension in a netCDF file.";
291         String footer = "\nExample: ncdefinedim -outputFileName ABC.nc -dimensionName Lat -dimensionSize 560\n"
292                 + "Will add a dimension called Lat allowing a maximum of 560 values to the file ABC.nc. "
293                 + "The file will be created if it doesn't already exist. ";
294         StringWriter sw = new StringWriter();
295         HelpFormatter formatter = new HelpFormatter();
296         formatter.setOptionComparator(new CommandLineOptionsComparator());
297         formatter.printHelp(new PrintWriter(sw), PRINT_WIDTH, NcDefineDimension.NC_DEFINE_DIM_COMMAND_NAME, header,
298                 this.options, 0, 1, footer);
299         return sw.toString();
300     }
301 
302     /*
303      * (non-Javadoc)
304      * 
305      * @see au.csiro.netcdf.Command#createOptions()
306      */
307     @SuppressWarnings("static-access")
308     @Override
309     public Options createOptions()
310     {
311         Option outputFileName = OptionBuilder.withArgName("file").hasArg().withDescription(
312                 "1: the filename of the netCDF file to be created.").isRequired(true).withLongOpt(OUTPUT_FILE).create(
313                 "o");
314 
315         // if a dimensionSize is not specified, then the dimension is "unlimited" by default.
316         Option dimensionSize = OptionBuilder.withArgName("integer").hasArg().withDescription(
317                 "2: the size of the dimension, OPTIONAL, if not specified dimension is considered to be \"unlimited\".")
318                 .isRequired(false).withLongOpt(DIMENSION_SIZE).create("s");
319 
320         Option dimensionName = OptionBuilder.withArgName("text").hasArg().withDescription(
321                 "3: the name to be given to the dimension.").isRequired(true).withLongOpt(DIMENSION_NAME).create("d");
322         
323         Option largeFileSupport = OptionBuilder.withDescription(
324                 "4: OPTIONAL, set if more than 2 GB of data will need to be stored in this file.").isRequired(false)
325                 .withLongOpt(IS_LARGE_FILE).create("l");
326         
327         Options options = new Options();
328 
329         options.addOption(outputFileName);
330         options.addOption(dimensionSize);
331         options.addOption(dimensionName);
332         options.addOption(largeFileSupport);
333 
334         return options;
335     }
336 
337     /**
338      * @return the usage for this command.
339      */
340     public String getUsageString()
341     {
342         return this.toString();
343     }
344 
345     /**
346      * Determine if this command is valid, check its syntax.
347      * 
348      * @param commandLine
349      *            command line arguments.
350      * @return error if found.
351      */
352     public String validCommand(String[] commandLine)
353     {
354         String errorMsg = "";
355 
356         // validate the command line parameters
357         try
358         {
359             // try parsing the command line, where ParseException can be thrown from
360             CommandLine parsedCommandLine = new BasicParser().parse(this.options, commandLine);
361             
362             // get the command line argument values
363             String outputFilenameArg = (parsedCommandLine.hasOption(OUTPUT_FILE)) ? parsedCommandLine
364                     .getOptionValue(OUTPUT_FILE) : "";              
365             String dimensionSizeArg = (parsedCommandLine.hasOption(DIMENSION_SIZE)) ? parsedCommandLine
366                     .getOptionValue(DIMENSION_SIZE) : "";
367             boolean isLargeFileSupport = parsedCommandLine.hasOption(IS_LARGE_FILE);  
368                     
369             // check whether large file support is needed
370             if (Util.fileExists(outputFilenameArg) 
371                     && Util.getExistingFile(outputFilenameArg).length() >= MAX_32BIT_OFFSET_FILE_SIZE
372                     && !isLargeFileSupport)
373             {
374                     throw new IllegalArgumentException("The netCDF file will be too large, please use " + IS_LARGE_FILE
375                             + " flag.");
376             }
377                     
378             // try getting dimension size, where IllegalArgumentException can be thrown from
379             if (!dimensionSizeArg.isEmpty())
380             {
381                 NcDefineDimension.mapStringToDimensionSize(dimensionSizeArg);
382             }
383         }
384         catch (ParseException pe)
385         {
386             errorMsg = errorMsg + "\n" + pe.getMessage();
387         }
388         catch (IllegalArgumentException iae)
389         {
390             errorMsg = errorMsg + "\n" + iae.getMessage();
391         }
392 
393         return errorMsg;
394     }
395 
396     /**
397      * Maps a <code>String</code> into an <code>integer</code>.
398      * 
399      * @param dimensionSizeArg
400      *            a dimensions size.
401      * @return the dimension size as an <code>integer</code>.
402      * @throws IllegalArgumentException
403      *             thrown if the <code>String</code> can not be mapped to a <code>integer</code>.
404      */
405     private static int mapStringToDimensionSize(String dimensionSizeArg)
406     {
407         if (Util.validateNumber(dimensionSizeArg))
408         {
409             return Integer.valueOf(dimensionSizeArg).intValue();
410         }
411         else
412         {
413             throw new IllegalArgumentException(NcDefineDimension.DIMENSION_SIZE + " value is not an integer: "
414                     + dimensionSizeArg);
415         }
416     }
417     
418     /**
419      * Check that a {@link Dimension} can be added to a netCDF-3 file.
420      * 
421      * @param ncfile
422      *            the netCDF-3 file that the {@link Dimension} will be added too.
423      * @param dimension
424      *            the {@link Dimension} to be added.
425      * @throws IllegalStateException
426      *             thrown if the {@link Dimension} can not be added to the netCDF-3 file.
427      */
428     private void checkDimensionLimitiations(NetcdfFileWriteable ncfile, Dimension dimension)
429             throws IllegalStateException
430     {
431         List<Dimension> dimensions = new ArrayList<Dimension>();
432         try
433         {
434             dimensions = ncfile.getDimensions();
435         }
436         catch (NullPointerException npe)
437         {
438             // the netcdf-java-4.0.41.jar will throw a NullPointerException if getDimensions() is called on a file that
439             // has no dimensions defined.
440             LOG.info("NetCDF file has no dimensions defined.");
441         }
442         
443         // throw an exception if the user is attempting to add an unlimited dimension to a netCDF-3
444         // file that already contains one. (this a limitation of the netCDF-3 file format). 
445         if (!dimensions.isEmpty() && ncfile.getUnlimitedDimension() != null && dimension.isUnlimited())
446         {
447             throw new IllegalStateException("Can not add an unlimited dimension to a netCDF-3 file "
448                     + "that already contains an unlimited dimension: " + ncfile.getUnlimitedDimension());
449         }
450     }
451 }