001/////////////////////////////////////////////////////////////////////////////
002// Copyright (c) 2009 OPeNDAP, Inc.
003// All rights reserved.
004// Permission is hereby granted, without written agreement and without
005// license or royalty fees, to use, copy, modify, and distribute this
006// software and its documentation for any purpose, provided that the above
007// copyright notice and the following two paragraphs appear in all copies
008// of this software.
009//
010// IN NO EVENT SHALL OPeNDAP BE LIABLE TO ANY PARTY FOR DIRECT, INDIRECT,
011// SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OF
012// THIS SOFTWARE AND ITS DOCUMENTATION, EVEN IF OPeNDAP HAS BEEN ADVISED
013// OF THE POSSIBILITY OF SUCH DAMAGE.
014//
015// OPeNDAP SPECIFICALLY DISCLAIMS ANY WARRANTIES, INCLUDING, BUT NOT
016// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
017// PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS ON AN "AS IS"
018// BASIS, AND OPeNDAP HAS NO OBLIGATION TO PROVIDE MAINTENANCE, SUPPORT,
019// UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
020//
021// Author: Nathan David Potter  <ndp@opendap.org>
022// You can contact OPeNDAP, Inc. at PO Box 112, Saunderstown, RI. 02874-0112.
023//
024/////////////////////////////////////////////////////////////////////////////
025
026package org.kepler.dataproxy.datasource.opendap;
027
028import java.io.IOException;
029import java.util.Collection;
030import java.util.Enumeration;
031import java.util.Iterator;
032import java.util.Vector;
033
034import org.apache.commons.logging.Log;
035import org.apache.commons.logging.LogFactory;
036import org.ecoinformatics.seek.datasource.DataSourceIcon;
037
038import opendap.dap.DAP2Exception;
039import opendap.dap.DAS;
040import opendap.dap.DArray;
041import opendap.dap.DConnect2;
042import opendap.dap.DConstructor;
043import opendap.dap.DDS;
044import opendap.dap.parser.ParseException;
045import ptolemy.actor.TypedIOPort;
046import ptolemy.actor.lib.LimitedFiringSource;
047import ptolemy.actor.parameters.PortParameter;
048import ptolemy.data.StringToken;
049import ptolemy.data.expr.StringParameter;
050import ptolemy.data.type.BaseType;
051import ptolemy.data.type.Type;
052import ptolemy.kernel.CompositeEntity;
053import ptolemy.kernel.util.IllegalActionException;
054import ptolemy.kernel.util.NameDuplicationException;
055
056/**
057 * The OPeNDAP actor reads data from OPeNDAP data sources (i.e. servers).
058 * <p/>
059 * <h1>OPeNDAP Actor Overview</h1>
060 * <p/>
061 * The OPeNDAP actor provides access to data served by any Data Access Protocol
062 * (DAP) 2.0 compatible data source. The actor takes as configuration parameters
063 * the URL to the data source and an optional constraint expression (CE). Based
064 * on the URL and optional CE, the actor configures its output ports to match
065 * the variables to be read from the data source.
066 * <p/>
067 * <h2>More information about the OPeNDAP actor</h2>
068 * <p/>
069 * The OPeNDAP actor reads data from a single DAP data server and provides that
070 * data as either a vector.matrix or array for processing by downstream elements
071 * in a Kepler workflow. Each DAP server provides (serves) many data sources and
072 * each of those data sources can be uniquely identified using a URL in a way
073 * that's similar to how pages are provided by a web server. For more
074 * information on the DAP and on OPeNDAP's software, see www.opendap.org.
075 * <p/>
076 * <h3>Characterization of Data Sources</h3>
077 * <p/>
078 * Data sources accessible using DAP 2.0 are characterized by a URL that
079 * references a both a specific data server and a data granule available from
080 * that server and a Constraint Expression that describes which variables to
081 * read from within the data granule. In addition to reading data from a
082 * granule, a DAP 2.0 server can provide two pieces of information about the
083 * granule: a description of all of its variables, their names and their data
084 * types; and a collection of 'attributes' which are bound to those variables.
085 * <p/>
086 * <h3>Operation of the Actor</h3>
087 * <p/>
088 * The actor must have a valid URL before it can provide any information (just
089 * as a file reader actor need to point toward a file to provide data). Given a
090 * URL and the optional CE, the OPeNDAP actor will interrogate that data source
091 * and configure its output ports.
092 * <p/>
093 * <h3>Data Types Returned by the Actor</h3>
094 * <p/>
095 * There are two broad classes of data types returned by the actor. First there
096 * are vectors, matrices and arrays. These correspond to one, two and N (&gt; 2)
097 * dimensional arrays. The distinction between the vector and matrix types and
098 * the N-dimensional array is that Kepler can operate on the vector and matrix
099 * types far more efficiently than the N-dimensional arrays. Many variables
100 * present in DAP data sources are of the N-dimensional array class and one way
101 * to work with these efficiently is to use the constraint expression to reduce
102 * the order of these data to one or two, thus causing the actor to store them
103 * in a vector or matrix.
104 * <p/>
105 * <p>
106 * As an example, consider the FNOC1 data source available at test.opendap.org.
107 * The full URL for this is http://test.opendap.org/opendap/data/nc/fnoc1.nc. It
108 * contains a variable 'u' which has three dimensions. We can constrain 'u' so
109 * that it has only two dimensions when read into Kepler using the CE
110 * 'u[0][0:16][0:20]' which selects only the first element (index 0) for the
111 * first dimension while requesting all of the remaining elements for the second
112 * and third dimensions. The www.opendap.org has documentation about the CE
113 * syntax.
114 * </p>
115 * <p/>
116 * <p>
117 * The second data type returned by the actor is a record. In reality, all DAP
118 * data sources are records but the actor automatically 'disassembles' the top
119 * most record since we know that's what the vast majority of users will want.
120 * However, some data sources contains nested hierarchies of records many levels
121 * deep. When dealing with those data sources you will need to use the Kepler
122 * record disassembler in your workflow.
123 * </p>
124 * 
125 * @author Nathan Potter
126 * @version $Id: OpendapDataSource.java 33629 2015-08-24 22:43:10Z crawl $
127 * @date Jul 17, 2007
128 * @since Kepler 1.0RC1
129 */
130public class OpendapDataSource extends LimitedFiringSource {
131
132        static Log log;
133
134        static {
135                log = LogFactory
136                                .getLog("org.kepler.dataproxy.datasource.opendap.OpendapDataSource");
137        }
138
139        /**
140         * The OPeNDAP URL that identifies a (possibly constrained) dataset.
141         */
142        public PortParameter opendapURLParameter;
143        private String opendapURL;
144
145        /**
146         * The OPeNDAP Constraint Expression used to sub sample the dataset.
147         */
148        public PortParameter opendapCEParameter;
149        private String opendapCE;
150
151        /**
152         * Controls if and how the DAP2 metadata is incorporated into the Actors
153         * output.
154         * 
155         */
156        public StringParameter metadataOptionsParameter;
157        public static int NO_METADATA = 0;
158        public static int EMBEDDED_METADATA = 1;
159        public static int SEPARATE_METADATA_PORT = 2;
160        private String[] metadataChoices = { "No Metadata", "Embedded Metadata",
161                        "Separate Metadata Port" };
162
163        public static String separateMetadataPortName = "DAP2 Metadata";
164        public static String globalMetadataPortName = "Global Metadata";
165
166        private boolean imbedMetadata;
167        private boolean useSeparateMetadataPort;
168
169        private DConnect2 dapConnection;
170        private DataSourceIcon icon;
171
172        public OpendapDataSource(CompositeEntity container, String name)
173                        throws NameDuplicationException, IllegalActionException {
174                super(container, name);
175
176                // hide the parent class's output port since we cannot delete it
177                new ptolemy.kernel.util.Attribute(output, "_hide");
178
179                opendapURLParameter = new PortParameter(this, "DAP2 URL");
180                opendapURLParameter.setStringMode(true);
181                opendapURLParameter.getPort().setTypeEquals(BaseType.STRING);
182                opendapURL = "";
183
184                opendapCEParameter = new PortParameter(this,
185                                "DAP2 Constraint Expression");
186                opendapCEParameter.setStringMode(true);
187                opendapCEParameter.getPort().setTypeEquals(BaseType.STRING);
188                opendapCE = "";
189
190                metadataOptionsParameter = new StringParameter(this, "Metadata Options");
191                metadataOptionsParameter.setTypeEquals(BaseType.STRING);
192                metadataOptionsParameter.setToken(new StringToken(metadataChoices[0]));
193                for (String choice : metadataChoices) {
194                        metadataOptionsParameter.addChoice(choice);
195                }
196                imbedMetadata = false;
197                useSeparateMetadataPort = false;
198
199                dapConnection = null;
200        try{
201                icon = new DataSourceIcon(this);
202        }catch(Throwable ex){
203                log.error(ex.getMessage());
204        }
205        }
206
207        /**
208         * @param attribute
209         *            The changed Attribute.
210         * @throws ptolemy.kernel.util.IllegalActionException
211         *             When bad things happen.
212         */
213        public void attributeChanged(ptolemy.kernel.util.Attribute attribute)
214                        throws ptolemy.kernel.util.IllegalActionException {
215
216                if (attribute == opendapURLParameter || attribute == opendapCEParameter
217                                || attribute == metadataOptionsParameter) {
218                        updateParameters();
219                } else {
220                        super.attributeChanged(attribute);
221                }
222        }
223
224        /**
225         * Update values in URL and CE parameters.
226         * 
227         * @throws IllegalActionException
228         *             When the bad things happen.
229         */
230        private void updateParameters() throws IllegalActionException {
231
232                boolean reload = false;
233
234                String url = ((StringToken) opendapURLParameter.getToken())
235                                .stringValue();
236                if (!opendapURL.equals(url)) {
237                        opendapURL = url;
238
239                        if (opendapURL.contains("?"))
240                                throw new IllegalActionException(this,
241                                                "The DAP2 URL must NOT contain a constraint expression or fragment thereof.");
242
243                        // only reload if not empty.
244                        if (!url.equals("")) {
245                                reload = true;
246                        }
247
248                }
249
250                String ce = ((StringToken) opendapCEParameter.getToken()).stringValue();
251                if (!opendapCE.equals(ce)) {
252                        opendapCE = ce;
253
254                        if (!opendapURL.equals("")) // Only REload if it looks like they
255                                                                                // have a URL too.
256                                reload = true;
257                }
258
259                String mdo = metadataOptionsParameter.stringValue();
260                boolean goodChoice = false;
261                for (String choice : metadataChoices) {
262                        if (mdo.equals(choice))
263                                goodChoice = true;
264                }
265                if (goodChoice) {
266
267                        if (mdo.equals(metadataChoices[NO_METADATA])) {
268
269                                if (imbedMetadata || useSeparateMetadataPort)
270                                        reload = true;
271
272                                imbedMetadata = false;
273                                useSeparateMetadataPort = false;
274                        } else if (mdo.equals(metadataChoices[EMBEDDED_METADATA])) {
275
276                                if (!imbedMetadata || useSeparateMetadataPort)
277                                        reload = true;
278
279                                imbedMetadata = true;
280                                useSeparateMetadataPort = false;
281                        } else if (mdo.equals(metadataChoices[SEPARATE_METADATA_PORT])) {
282
283                                if (imbedMetadata || !useSeparateMetadataPort)
284                                        reload = true;
285
286                                imbedMetadata = false;
287                                useSeparateMetadataPort = true;
288                        }
289
290                } else {
291                        String msg = "You may not edit the metadata options. "
292                                        + "You must chose one of: ";
293                        for (String choice : this.metadataChoices) {
294                                msg += "[" + choice + "]   ";
295                        }
296                        throw new IllegalActionException(this, msg);
297                }
298
299                if (reload) {
300
301                        try {
302
303                                log.debug("OPeNDAP URL: " + opendapURL);
304                                dapConnection = new DConnect2(opendapURL);
305
306                                DDS dds = getDDS(ce, false);
307
308                                // log.debug("Before ports configured.");
309                                // dds.print(System.out);
310
311                                log.debug("Configuring ports.");
312                                configureOutputPorts(dds);
313
314                                // log.debug("After ports configured.");
315                                // dds.print(System.out);
316
317                        } catch (Exception e) {
318                                e.printStackTrace();
319                                throw new IllegalActionException(this, "Problem accessing "
320                                                + "OPeNDAP Data Source: " + e.getMessage());
321                        }
322
323                }
324        }
325
326        public void preinitialize() throws IllegalActionException {
327                super.preinitialize();
328
329                String ce;
330                ce = ((StringToken) opendapCEParameter.getToken()).stringValue();
331                log.debug("opendapCEParameter: " + ce);
332
333                if (ce.equals("")) {
334                        ce = createCEfromWiredPorts();
335                        log.debug("Created CE from wired ports. CE: " + ce);
336                        opendapCEParameter.setToken(new StringToken(ce));
337                        updateParameters();
338                }
339                log.debug("metadataOptionsParameter.stringValue(): "
340                                + metadataOptionsParameter.stringValue());
341                log.debug("ce: "
342                                + ((StringToken) opendapCEParameter.getToken()).stringValue());
343
344        }
345
346        public void fire() throws IllegalActionException {
347                super.fire();
348                log.debug("\n\n\n--- fire");
349
350                opendapURLParameter.update();
351                opendapCEParameter.update();
352                updateParameters();
353
354                try {
355                        if (icon != null)
356                                icon.setBusy();
357
358                        String ce;
359                        ce = ((StringToken) opendapCEParameter.getToken()).stringValue();
360
361                        log.debug("ConstraintExpression: " + ce);
362
363                        DDS dds = getDDS(ce, true);
364
365                        // log.debug("fire(): dapConnection.getData(ce) returned DataDDS:");
366
367                        log.debug("Broadcasting DAP data arrays.");
368                        broadcastDapData(dds);
369                        
370                        if (icon != null)
371                                icon.setReady();
372
373                        // log.debug("fire(): After data broadcast:");
374                        // dds.print(System.out);
375
376                } catch (Exception e) {
377                        log.error("fire() Failed: ", e);
378
379                }
380        }
381
382        /**
383         * Build up the projection part of the constraint expression (CE) in order
384         * to minimize the amount of data retrieved. If the CE is empty, then this
385         * will build a list of projected variables based on which output ports are
386         * wired. If the CE is not empty then it will not be modified.
387         * 
388         * @return A new CE if the passed one is not empty, a new one corresponding
389         *         to the wired output ports otherwise.
390         * @exception IllegalActionException
391         *                If thrown will getting the width of the ports.
392         */
393        private String createCEfromWiredPorts() throws IllegalActionException {
394
395                String ce;
396
397                // Get the port list
398                Iterator i = this.outputPortList().iterator();
399
400                String projection = "";
401                int pcount = 0;
402                while (i.hasNext()) {
403                        TypedIOPort port = (TypedIOPort) i.next();
404
405                        if (port.getWidth() > 0
406                                        && !port.getName().equals(separateMetadataPortName)) {
407
408                                log.debug("Added " + port.getName() + " to projection.");
409                                if (pcount > 0)
410                                        projection += ",";
411                                projection += port.getName();
412                                pcount++;
413                        }
414                }
415                ce = projection;
416
417                return ce;
418
419        }
420
421        /**
422         * Walks through the DDS, converts DAP data to ptII data, and broadcasts the
423         * data onto the appropriate ports.
424         * 
425         * @param dds
426         *            The DDS from which to get the data to send
427         * @throws IllegalActionException
428         *             When bad things happen.
429         */
430        private void broadcastDapData(DDS dds) throws IllegalActionException {
431
432                // log.debug("broadcastDapData(): DataDDS prior to broadcast:");
433                // dds.print(System.out);
434
435                TypedIOPort port;
436
437                // log.debug("Broadcasting Dap Data for DDS: ");
438                // dds.print(System.out);
439
440                if (useSeparateMetadataPort) {
441                        log.debug("Sending " + separateMetadataPortName + " data.");
442                        port = (TypedIOPort) this.getPort(separateMetadataPortName);
443                        port.broadcast(AttTypeMapper.buildMetaDataTokens(dds));
444                        log.debug("Sent " + separateMetadataPortName);
445                }
446
447                if (imbedMetadata) {
448                        log.debug("Sending " + globalMetadataPortName + " port data.");
449                        port = (TypedIOPort) this.getPort(globalMetadataPortName);
450                        port.broadcast(AttTypeMapper.convertAttributeToToken(dds
451                                        .getAttribute()));
452                        log.debug("Sent " + globalMetadataPortName);
453                }
454
455                Enumeration e = dds.getVariables();
456                while (e.hasMoreElements()) {
457                        opendap.dap.BaseType bt = (opendap.dap.BaseType) e.nextElement();
458
459                        String columnName = TypeMapper.replacePeriods(bt.getName().trim());
460                        // Get the port associated with this DDS variable.
461                        port = (TypedIOPort) this.getPort(columnName);
462                        if (port == null) {
463                                throw new IllegalActionException(this,
464                                                "Request Output Port Missing: " + columnName);
465                        }
466
467                        log.debug("Translating data.");
468                        // bt.printDecl(System.out);
469
470                        // Map the DAP data for this variable into the ptII Token model.
471                        ptolemy.data.Token token = TokenMapper.mapDapObjectToToken(bt,
472                                        imbedMetadata);
473                        log.debug("Data Translated.");
474                        // bt.printDecl(System.out);
475
476                        // Send the data.
477                        log.debug("Sending data.");
478                        port.broadcast(token);
479                        log.debug("Sent data.");
480
481                }
482
483        }
484
485        /**
486         * Configure the output ports to expose all of the variables at the top
487         * level of the (potentially constrained) DDS.
488         * 
489         * @param dds
490         *            The DDS
491         * @throws IllegalActionException
492         *             When bad things happen.
493         */
494        private void configureOutputPorts(DDS dds) throws IllegalActionException {
495
496                Vector<Type> types = new Vector<Type>();
497                Vector<String> names = new Vector<String>();
498
499                if (useSeparateMetadataPort) {
500                        log.debug("Adding " + separateMetadataPortName
501                                        + " port to port list.");
502                        names.add(separateMetadataPortName);
503                        types.add(AttTypeMapper.buildMetaDataTypes(dds));
504                        log.debug("Added " + separateMetadataPortName
505                                        + " port to port list.");
506                }
507
508                if (imbedMetadata) {
509                        log.debug("Adding " + globalMetadataPortName
510                                        + " port to port list.");
511                        names.add(globalMetadataPortName);
512                        types.add(AttTypeMapper.convertAttributeToType(dds.getAttribute()));
513                        log.debug("Added " + globalMetadataPortName + " port.");
514                }
515
516                Enumeration e = dds.getVariables();
517                while (e.hasMoreElements()) {
518                        opendap.dap.BaseType bt = (opendap.dap.BaseType) e.nextElement();
519                        types.add(TypeMapper.mapDapObjectToType(bt, imbedMetadata));
520                        names.add(TypeMapper.replacePeriods(bt.getName()));
521                }
522
523                removeOtherOutputPorts(names);
524
525                Iterator ti = types.iterator();
526                Iterator ni = names.iterator();
527
528                while (ti.hasNext() && ni.hasNext()) {
529                        Type type = (Type) ti.next();
530                        String name = (String) ni.next();
531                        initializePort(name, type);
532                }
533
534        }
535
536        /**
537         * Add a new port.
538         * 
539         * @param aPortName
540         *            name of new port
541         * @param aPortType
542         *            Type of new port
543         * @throws IllegalActionException
544         *             When bad things happen.
545         */
546        void initializePort(String aPortName, Type aPortType)
547                        throws IllegalActionException {
548                try {
549                        String columnName = aPortName.trim();
550                        // Create a new port for each Column in the resultset
551                        TypedIOPort port = (TypedIOPort) this.getPort(columnName);
552                        boolean aIsNew = (port == null);
553                        if (aIsNew) {
554                                // Create a new typed port and add it to this container
555                                port = new TypedIOPort(this, columnName, false, true);
556                                new ptolemy.kernel.util.Attribute(port, "_showName");
557                                log.debug("Creating port [" + columnName + "]" + this);
558                        }
559
560                        // FIXME: we cannot set the port type during fire() since this
561                        // requires write access to the workspace, which could lead to
562                        // a deadlock. For now, we check to see if the port type is
563                        // different; it usually is the same when we're in fire().
564
565                        // See if the port types are different.
566                        if (!port.getType().equals(aPortType)) {
567                                port.setTypeEquals(aPortType);
568                        }
569
570                } catch (ptolemy.kernel.util.NameDuplicationException nde) {
571                        throw new IllegalActionException(this,
572                                        "One or more attributes has the same name.  Please correct this and try again.");
573                }
574
575        }
576
577        /**
578         * Remove all ports which's name is not in the selected vector
579         * 
580         * @param nonRemovePortName
581         *            The ports to NOT remove. That means keep. Whatever...
582         * @throws IllegalActionException
583         *             When bad things happen.
584         */
585        void removeOtherOutputPorts(Collection nonRemovePortName)
586                        throws IllegalActionException {
587                // Use toArray() to make a deep copy of this.portList().
588                // Do this to prevent ConcurrentModificationExceptions.
589                TypedIOPort[] l = new TypedIOPort[0];
590                l = (TypedIOPort[]) this.portList().toArray(l);
591
592                for (TypedIOPort port : l) {
593                        if (port == null || port.isInput()) {
594                                continue;
595                        }
596                        String currPortName = port.getName();
597
598                        // Do not remove the output port since it belongs to a
599                        // parent class.
600                        if (!nonRemovePortName.contains(currPortName)
601                                        && !currPortName.equals("output")) {
602                                try {
603                                        port.setContainer(null);
604                                } catch (Exception ex) {
605                                        throw new IllegalActionException(this,
606                                                        "Error removing port: " + currPortName);
607                                }
608                        }
609                }
610        }
611
612        /**
613         * Remove all ports.
614         * 
615         * @throws IllegalActionException
616         *             When bad things happen.
617         */
618        void removeAllOutputPorts() throws IllegalActionException {
619                // Use toArray() to make a deep copy of this.portList().
620                // Do this to prevent ConcurrentModificationExceptions.
621                TypedIOPort[] ports = new TypedIOPort[0];
622                ports = (TypedIOPort[]) this.portList().toArray(ports);
623
624                for (TypedIOPort port : ports) {
625                        if (port != null && port.isOutput()) {
626                                String currPortName = port.getName();
627                                try {
628                                        port.setContainer(null);
629                                } catch (Exception ex) {
630                                        throw new IllegalActionException(this,
631                                                        "Error removing port: " + currPortName);
632                                }
633                        }
634
635                }
636        }
637
638        /**
639         * Probe a port
640         * 
641         * @param port
642         *            The port to probe.
643         * @return The probe report.
644         */
645        public static String portInfo(TypedIOPort port) {
646                String width = "";
647                try {
648                        width = Integer.valueOf(port.getWidth()).toString();
649                } catch (IllegalActionException ex) {
650                        width = "Failed to get width of port " + port.getFullName() + ex;
651                }
652
653                String description = "";
654                try {
655                        description = port.description();
656                } catch (IllegalActionException ex) {
657                        description = "Failed to get the description of port "
658                                        + port.getFullName() + ": " + ex;
659                }
660                String msg = "Port Info: \n";
661                msg += "    getName():         " + port.getName() + "\n";
662                msg += "    getWidth():        " + width + "\n";
663                msg += "    isInput():         " + port.isInput() + "\n";
664                msg += "    isOutput():        " + port.isOutput() + "\n";
665                msg += "    isMultiport():     " + port.isMultiport() + "\n";
666                msg += "    className():       " + port.getClassName() + "\n";
667                msg += "    getDisplayName():  " + port.getDisplayName() + "\n";
668                msg += "    getElementName():  " + port.getElementName() + "\n";
669                msg += "    getFullName():     " + port.getFullName() + "\n";
670                msg += "    getSource():       " + port.getSource() + "\n";
671                msg += "    description():     " + description + "\n";
672                msg += "    toString():        " + port + "\n";
673                return msg;
674        }
675
676        private DDS getDDS(String ce, boolean getData) throws IOException,
677                        ParseException, DAP2Exception {
678
679                DDS dds, ddx;
680                DAS das;
681
682                /*
683                 * ------------------------------------------------------------- Get the
684                 * metadata - this is ugly because there is a bug in DConnect that
685                 * causes calls to getDataDDX to return a DDS object that contains no
686                 * data.
687                 */
688
689                try {
690                        log.debug("Attempting to get DDX.");
691
692                        ddx = dapConnection.getDDX(ce);
693
694                        log.debug("Got DDX.");
695                        // ddx.print(System.out);
696                } catch (Exception e) {
697                        log.debug("Failed to get DataDDX. Msg: " + e.getMessage());
698
699                        log.debug("Attempting to get DDS. ce: " + ce);
700                        ddx = dapConnection.getDDS(ce);
701                        log.debug("Got DDS.");
702
703                        log.debug("Attempting to get DAS.");
704                        das = dapConnection.getDAS();
705                        log.debug("Got DAS.");
706
707                        log.debug("Calling DDS.ingestDAS().");
708                        ddx.ingestDAS(das);
709
710                }
711                log.debug("Squeezing DDX arrays.");
712                squeezeArrays(ddx);
713
714                if (getData) {
715                        // Get the data.
716                        log.debug("Attempting to get DataDDS.");
717                        dds = dapConnection.getData(ce);
718                        log.debug("Got DataDDS.");
719                        // dds.print(System.out);
720
721                        log.debug("Squeezing DDS arrays.");
722                        squeezeArrays(dds);
723
724                        // Extract the metadata we got from the ddx.
725                        log.debug("Retrieving DAS from DDX.");
726                        das = ddx.getDAS();
727
728                        // Tie the extracted metadata back into the DataDDS
729                        log.debug("Calling DDS.ingestDAS().");
730                        dds.ingestDAS(das);
731                        // dds.print(System.out);
732
733                        return dds;
734                }
735                /*
736                 * End of the ugly bit. Once the bug in DConnect2 is fixed we can
737                 * replace this with a call to getDataDDX().
738                 * 
739                 * -----------------------------------------------------------
740                 */
741                // dds.print(System.out);
742                return ddx;
743
744        }
745
746        /**
747         * Eliminates array dimensions whose dimensions are 1 (and thus in practice
748         * don't exist)
749         * 
750         * @param dds
751         *            The DDS to traverse and squeeze its member arrays.
752         */
753        public static void squeezeArrays(DConstructor dds) {
754
755                DArray a;
756
757                Enumeration e = dds.getVariables();
758                while (e.hasMoreElements()) {
759                        opendap.dap.BaseType bt = (opendap.dap.BaseType) e.nextElement();
760
761                        if (bt instanceof DArray) {
762                                a = (DArray) bt;
763                                log.debug("Squeezing array " + a.getTypeName() + " "
764                                                + a.getLongName() + ";");
765                                a.squeeze();
766                                // System.out.print("Post squeezing: ");
767                                // a.printDecl(System.out);
768                                bt = a.getPrimitiveVector().getTemplate();
769                                if (bt instanceof DConstructor)
770                                        squeezeArrays((DConstructor) bt);
771                        } else if (bt instanceof DConstructor) {
772                                squeezeArrays((DConstructor) bt);
773                        }
774
775                }
776        }
777
778}