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}