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}