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