001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2015
005 * Space Science and Engineering Center (SSEC)
006 * University of Wisconsin - Madison
007 * 1225 W. Dayton Street, Madison, WI 53706, USA
008 * https://www.ssec.wisc.edu/mcidas
009 * 
010 * All Rights Reserved
011 * 
012 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
013 * some McIDAS-V source code is based on IDV and VisAD source code.  
014 * 
015 * McIDAS-V is free software; you can redistribute it and/or modify
016 * it under the terms of the GNU Lesser Public License as published by
017 * the Free Software Foundation; either version 3 of the License, or
018 * (at your option) any later version.
019 * 
020 * McIDAS-V is distributed in the hope that it will be useful,
021 * but WITHOUT ANY WARRANTY; without even the implied warranty of
022 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
023 * GNU Lesser Public License for more details.
024 * 
025 * You should have received a copy of the GNU Lesser Public License
026 * along with this program.  If not, see http://www.gnu.org/licenses.
027 */
028
029package edu.wisc.ssec.mcidasv.data.hydra;
030
031import java.io.File;
032import java.io.IOException;
033import java.io.InputStream;
034import java.io.StringReader;
035import java.io.UnsupportedEncodingException;
036import java.net.URISyntaxException;
037import java.util.ArrayList;
038import java.util.Enumeration;
039import java.util.HashMap;
040import java.util.HashSet;
041import java.util.jar.JarEntry;
042import java.util.jar.JarFile;
043
044import javax.xml.parsers.DocumentBuilder;
045import javax.xml.parsers.DocumentBuilderFactory;
046import javax.xml.parsers.ParserConfigurationException;
047
048import org.slf4j.Logger;
049import org.slf4j.LoggerFactory;
050
051import org.w3c.dom.Document;
052import org.w3c.dom.Node;
053import org.w3c.dom.NodeList;
054
055import org.xml.sax.EntityResolver;
056import org.xml.sax.InputSource;
057import org.xml.sax.SAXException;
058
059import edu.wisc.ssec.mcidasv.data.QualityFlag;
060
061public class SuomiNPPProductProfile {
062        
063        private static final Logger logger = LoggerFactory.getLogger(SuomiNPPProductProfile.class);
064        
065        DocumentBuilder db = null;
066        // if we need to pull product profiles from the McV jar file
067        boolean readFromJar = false;
068        HashMap<String, String> rangeMin = new HashMap<String, String>();
069        HashMap<String, String> rangeMax = new HashMap<String, String>();
070        HashMap<String, String> units = new HashMap<String, String>();
071        HashMap<String, String> scaleFactorName = new HashMap<String, String>();
072        HashMap<String, ArrayList<Float>> fillValues = new HashMap<String, ArrayList<Float>>();
073        HashMap<String, ArrayList<QualityFlag>> qualityFlags = new HashMap<String, ArrayList<QualityFlag>>();
074
075        public SuomiNPPProductProfile() throws ParserConfigurationException, SAXException, IOException {
076
077        logger.trace("SuomiNPPProductProfile init...");
078                DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
079        factory.setNamespaceAware(false);
080        db = factory.newDocumentBuilder();
081        db.setEntityResolver(new EntityResolver()
082        {
083            public InputSource resolveEntity(String publicId, String systemId)
084                throws SAXException, IOException
085            {
086                return new InputSource(new StringReader(""));
087            }
088        });
089
090        }
091        
092        /**
093         * See if for a given N_Collection_Short_Name attribute, the profile is
094         * present.
095         *
096         * @param attrName The attribute name our file should match. {@code null}
097         * is allowed.
098         *
099         * @return Full file name for the XML Product Profile, or {@code null}.
100         */
101        
102        public String getProfileFileName(String attrName) {
103                
104                // sanity check
105                if (attrName == null) return null;
106                
107                // Locate the base app JAR file
108                File mcvJar = findMcVJar();
109                if (mcvJar == null) return null;
110
111                // we need to pull the XML Product Profiles out of mcidasv.jar
112                JarFile jar;
113                try {
114                        jar = new JarFile(mcvJar);
115                        // gives ALL entries in jar
116                        Enumeration<JarEntry> entries = jar.entries();
117                        boolean found = false;
118                        String name = null;
119                        while (entries.hasMoreElements()) {
120                                name = entries.nextElement().getName();
121                                // filter according to the profiles
122                                if (name.contains("XML_Product_Profiles")) { 
123                                        if (name.contains(attrName + "-PP")) {
124                                                found = true;
125                                                break;
126                                        }
127                                }
128                        }
129                        jar.close();
130                        if (found == true) {
131                                logger.trace("Found profile: " + name);
132                                return name;
133                        }
134                } catch (UnsupportedEncodingException e) {
135                        e.printStackTrace();
136                        return null;
137                } catch (IOException e) {
138                        e.printStackTrace();
139                        return null;
140                }
141
142                return null;
143        }
144
145        /**
146         * Attempts to locate {@code mcidasv.jar} within the
147         * {@literal "classpath"}.
148         *
149         * @return {@code File} object which for mcidasv.jar, or {@code null} if
150         * not found
151         */
152        
153        private File findMcVJar() {
154                File mcvJar = null;
155                try {
156                        mcvJar = new File(getClass().getProtectionDomain().getCodeSource().getLocation().toURI());
157                } catch (URISyntaxException urise) {
158                        // just log the exception for analysis
159                        urise.printStackTrace();
160                }
161                return mcvJar;
162        }
163        
164        @SuppressWarnings("deprecation")
165        public void addMetaDataFromFile(String fileName) throws SAXException, IOException {
166                
167                File mcvJar = findMcVJar();
168                if (mcvJar == null) {
169                        logger.error("Unable to parse Suomi XML Product Profile");
170                        return;
171                }
172
173                Document d = null;
174                InputStream ios = null;
175                JarFile jar = new JarFile(mcvJar);
176                JarEntry je = jar.getJarEntry(fileName);
177                ios = jar.getInputStream(je);
178                d = db.parse(ios);
179                
180                NodeList nl = d.getElementsByTagName("Field");
181                for (int i = 0; i < nl.getLength(); i++) {
182                        ArrayList<Float> fValAL = new ArrayList<Float>();
183                        ArrayList<QualityFlag> qfAL = new ArrayList<QualityFlag>();
184                        Node n = nl.item(i);
185                        NodeList children = n.getChildNodes();
186                        NodeList datum = null;
187                        String name = null;
188                        boolean isQF = false;
189                        // temporary set used to guarantee unique names within quality flag
190                        HashSet<String> hs = new HashSet<String>();
191                        int uniqueCounter = 2;
192                        
193                        // cycle through once, finding name and datum node(s)
194                        // NOTE: may be multiple datum notes, e.g. QF quality flags
195                        for (int j = 0; j < children.getLength(); j++) {
196                                
197                                Node child = children.item(j);
198                                if (child.getNodeName().equals("Name")) {
199                                        name = child.getTextContent();
200                                        logger.debug("Found Suomi NPP product name: " + name);
201                                        if (name.startsWith("QF")) {
202                                                isQF = true;
203                                                uniqueCounter = 2;
204                                        }
205                                }
206                                
207                                if (child.getNodeName().equals("Datum")) {
208                                        
209                                        datum = child.getChildNodes();
210                                        String rMin = null;
211                                        String rMax = null;     
212                                        String unitStr = null;
213                                        String sFactorName = null;      
214
215                                        if ((name != null) && (datum != null)) {
216                                                
217                                                // if it's a quality flag, do separate loop 
218                                                // and store relevant info in a bean
219                                                if (isQF) {
220                                                        QualityFlag qf = null;
221                                                        HashMap<String, String> hm = new HashMap<String, String>();
222                                                        int bitOffset = -1;
223                                                        int numBits = -1;
224                                                        String description = null;
225                                                        boolean haveOffs = false;
226                                                        boolean haveSize = false;
227                                                        boolean haveDesc = false;
228                                                        for (int k = 0; k < datum.getLength(); k++) {
229                                                                Node datumChild = datum.item(k);
230                                                                if (datumChild.getNodeName().equals("DatumOffset")) {
231                                                                        String s = datumChild.getTextContent();
232                                                                        bitOffset = Integer.parseInt(s);
233                                                                        haveOffs = true;
234                                                                }
235                                                                if (datumChild.getNodeName().equals("DataType")) {
236                                                                        String s = datumChild.getTextContent();
237                                                                        // we will only handle the bit fields.
238                                                                        // others cause an exception so just catch and continue
239                                                                        try {
240                                                                                numBits = Integer.parseInt(s.substring(0, 1));
241                                                                        } catch (NumberFormatException nfe) {
242                                                                                continue;
243                                                                        }
244                                                                        haveSize = true;
245                                                                }
246                                                                if (datumChild.getNodeName().equals("Description")) {
247                                                                        String s = datumChild.getTextContent();
248                                                                        // first, special check for "Test" flags, want to 
249                                                                        // include the relevant bands on those.  Seem to
250                                                                        // directly follow test name in parens
251                                                                        boolean isTest = false;
252                                                                        if (s.contains("Test (")) {
253                                                                                isTest = true;
254                                                                                int idx = s.indexOf(")");
255                                                                                if (idx > 0) {
256                                                                                        description = s.substring(0, idx + 1);
257                                                                                }
258                                                                        } else {
259                                                                                // for non-Test flags, we DO want to
260                                                                                // lose any ancillary (in parentheses) info
261                                                                                int endIdx = s.indexOf("(");
262                                                                                if (endIdx > 0) {
263                                                                                        description = s.substring(0, endIdx);
264                                                                                } else {
265                                                                                        description = s;
266                                                                                }
267                                                                        }
268                                                                        // another "ancillary info weedout" check, sometimes
269                                                                        // there is long misc trailing info after " - "
270                                                                        if ((description.contains(" - ")) && !isTest) {
271                                                                                int idx = description.indexOf(" - ");
272                                                                                description = description.substring(0, idx);
273                                                                                boolean added = hs.add(name + description);
274                                                                                // if HashSet add fails, it's a dupe name.
275                                                                                // tack on incrementing digit to make it unique
276                                                                                if (! added) {
277                                                                                        description = description + "_" + uniqueCounter;
278                                                                                        hs.add(name + description);
279                                                                                        uniqueCounter++;
280                                                                                }
281                                                                        }
282                                                                        // ensure what's left is a valid NetCDF object name
283                                                                        description= ucar.nc2.iosp.netcdf3.N3iosp.makeValidNetcdf3ObjectName(description);
284                                                                        // valid name maker sometimes leaves trailing underscore - remove these
285                                                                        if (description.endsWith("_")) {
286                                                                                description = description.substring(0, description.length() - 1);
287                                                                        }
288                                                                        logger.debug("Final name: " + description);
289                                                                        haveDesc = true;
290                                                                }
291                                                                if (datumChild.getNodeName().equals("LegendEntry")) {
292                                                                        NodeList legendChildren = datumChild.getChildNodes();
293                                                                        boolean gotName = false;
294                                                                        boolean gotValue = false;
295                                                                        String nameStr = null;
296                                                                        String valueStr = null;
297                                                                        for (int legIdx = 0; legIdx < legendChildren.getLength(); legIdx++) { 
298                                                                                Node legendChild = legendChildren.item(legIdx);
299                                                                                if (legendChild.getNodeName().equals("Name")) {
300                                                                                        nameStr = legendChild.getTextContent();
301                                                                                        gotName = true;
302                                                                                }
303                                                                                if (legendChild.getNodeName().equals("Value")) {
304                                                                                        valueStr = legendChild.getTextContent();
305                                                                                        gotValue = true;
306                                                                                }
307                                                                        }
308                                                                        if (gotName && gotValue) {
309                                                                                hm.put(valueStr, nameStr);
310                                                                        }
311                                                                }
312                                                        }
313                                                        if (haveOffs && haveSize && haveDesc) {
314                                                                qf = new QualityFlag(bitOffset, numBits, description, hm);
315                                                                qfAL.add(qf);
316                                                        }
317                                                }
318                                                
319                                                for (int k = 0; k < datum.getLength(); k++) {
320                                                        
321                                                        Node datumChild = datum.item(k);
322                                                        if (datumChild.getNodeName().equals("RangeMin")) {
323                                                                rMin = datumChild.getTextContent();
324                                                        }
325                                                        if (datumChild.getNodeName().equals("RangeMax")) {
326                                                                rMax = datumChild.getTextContent();
327                                                        }
328                                                        if (datumChild.getNodeName().equals("MeasurementUnits")) {
329                                                                unitStr = datumChild.getTextContent();
330                                                        }
331                                                        if (datumChild.getNodeName().equals("ScaleFactorName")) {
332                                                                sFactorName = datumChild.getTextContent();
333                                                        }
334                                                        if (datumChild.getNodeName().equals("FillValue")) {
335                                                                // go one level further to datumChild element Value
336                                                                NodeList grandChildren = datumChild.getChildNodes();
337                                                                for (int l = 0; l < grandChildren.getLength(); l++) {
338                                                                        Node grandChild = grandChildren.item(l);
339                                                                        if (grandChild.getNodeName().equals("Value")) {
340                                                                                String fillValueStr = grandChild.getTextContent();
341                                                                                fValAL.add(new Float(Float.parseFloat(fillValueStr)));
342                                                                        }
343                                                                }
344                                                        }
345                                                }
346                                        }
347                                        
348                                        boolean rangeMinOk = false;
349                                        boolean rangeMaxOk = false;
350                                        
351                                        if ((name != null) && (rMin != null)) {
352                                                // make sure the field parses to a numeric value
353                                                try {
354                                                        Float.parseFloat(rMin);
355                                                        rangeMinOk = true;
356                                                } catch (NumberFormatException nfe) {
357                                                        // do nothing, just won't use ranges for this variable
358                                                }
359                                        }
360                                        
361                                        if ((name != null) && (rMax != null)) {
362                                                // make sure the field parses to a numeric value
363                                                try {
364                                                        Float.parseFloat(rMax);
365                                                        rangeMaxOk = true;
366                                                } catch (NumberFormatException nfe) {
367                                                        // do nothing, just won't use ranges for this variable
368                                                }
369                                        }
370                                        
371                                        // only use range if min and max checked out
372                                        if ((rangeMinOk) && (rangeMaxOk)) {
373                                                rangeMin.put(name, rMin);
374                                                rangeMax.put(name, rMax);
375                                        }
376                                        
377                                        if ((name != null) && (unitStr != null)) {
378                                                units.put(name, unitStr);
379                                        } else {
380                                                units.put(name, "Unknown");
381                                        }
382                                        
383                                        if ((name != null) && (sFactorName != null)) {
384                                                scaleFactorName.put(name, sFactorName);
385                                        }
386                                        
387                                        if ((name != null) && (! fValAL.isEmpty())) {
388                                                fillValues.put(name, fValAL);
389                                        }
390                                        
391                                        if ((name != null) && (! qfAL.isEmpty())) {
392                                                qualityFlags.put(name, qfAL);
393                                        }
394                                }
395                        }
396                }
397                if (ios != null) {
398                        try {
399                                ios.close();
400                                jar.close();
401                        } catch (IOException ioe) {
402                                // do nothing
403                        }
404                }
405        }
406
407        /**
408         * Check if this product profile has a product AND metadata.
409         *
410         * <p>Note: Checking presence of a Range alone is not sufficient.</p>
411         *
412         * @param name {@literal "Product"} name.
413         *
414         * @return true if both conditions met
415         */
416        
417        public boolean hasNameAndMetaData(String name) {
418                if ((rangeMin.containsKey(name) || (fillValues.containsKey(name)))) {
419                        return true;
420                } else {
421                        return false;
422                }
423        }
424        
425        public String getRangeMin(String name) {
426                return rangeMin.get(name);
427        }
428        
429        public String getRangeMax(String name) {
430                return rangeMax.get(name);
431        }
432        
433        public String getUnits(String name) {
434                return units.get(name);
435        }
436        
437        public String getScaleFactorName(String name) {
438                return scaleFactorName.get(name);
439        }
440        
441        public ArrayList<Float> getFillValues(String name) {
442                return fillValues.get(name);
443        }
444        
445        public ArrayList<QualityFlag> getQualityFlags(String name) {
446                return qualityFlags.get(name);
447        }
448
449}