001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2016 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; 030 031import static ucar.unidata.xml.XmlUtil.getAttribute; 032import static ucar.unidata.xml.XmlUtil.getChildText; 033import static ucar.unidata.xml.XmlUtil.getElements; 034 035import java.io.File; 036import java.io.IOException; 037import java.io.InputStream; 038import java.util.ArrayList; 039import java.util.Collections; 040import java.util.Hashtable; 041import java.util.LinkedHashMap; 042import java.util.List; 043import java.util.Map; 044 045import org.slf4j.Logger; 046import org.slf4j.LoggerFactory; 047import org.w3c.dom.Attr; 048import org.w3c.dom.Element; 049import org.w3c.dom.NamedNodeMap; 050import org.w3c.dom.NodeList; 051 052import ucar.unidata.idv.IdvResourceManager; 053import ucar.unidata.idv.IntegratedDataViewer; 054import ucar.unidata.idv.StateManager; 055import ucar.unidata.util.IOUtil; 056import ucar.unidata.util.ResourceCollection; 057import ucar.unidata.util.ResourceCollection.Resource; 058import ucar.unidata.util.StringUtil; 059 060/** 061 * McIDAS-V's resource manager. The chief differences from Unidata's 062 * {@link IdvResourceManager} are supporting {@literal "default"} McIDAS-V 063 * bundles, and some initial attempts at safer resource handling. 064 */ 065public class ResourceManager extends IdvResourceManager { 066 067 private static final Logger logger = LoggerFactory.getLogger(ResourceManager.class); 068 069 /** Points to the adde image defaults */ 070 public static final XmlIdvResource RSC_PARAMETERSETS = 071 new XmlIdvResource("idv.resource.parametersets", 072 "Chooser Parameter Sets", "parametersets\\.xml$"); 073 074 public static final IdvResource RSC_SITESERVERS = 075 new XmlIdvResource("mcv.resource.siteservers", 076 "Site-specific Servers", "siteservers\\.xml$"); 077 078 public static final IdvResource RSC_NEW_USERSERVERS = 079 new XmlIdvResource("mcv.resource.newuserservers", 080 "New style user servers", "persistedservers\\.xml$"); 081 082 public static final IdvResource RSC_OLD_USERSERVERS = 083 new XmlIdvResource("mcv.resource.olduserservers", 084 "Old style user servers", "addeservers\\.xml$"); 085 086 public ResourceManager(IntegratedDataViewer idv) { 087 super(idv); 088 checkMoveOutdatedDefaultBundle(); 089 } 090 091 /** 092 * Overridden so that McIDAS-V can attempt to verify {@literal "critical"} 093 * resources without causing crashes. 094 * 095 * <p>Currently doesn't do a whole lot. 096 * 097 * @see #verifyResources() 098 */ 099 @Override protected void init(List rbiFiles) { 100 super.init(rbiFiles); 101// verifyResources(); 102 } 103 104 /** 105 * Loops through all of the {@link ResourceCollection}s that the IDV knows 106 * about. 107 * 108 * <p>I realize that this could balloon into a really tedious thing... 109 * there could potentially be verification steps for each type of resource 110 * collection! the better approach is probably to identify a few key collections 111 * (like the (default?) maps). 112 */ 113 protected void verifyResources() { 114 List<IdvResource> resources = new ArrayList<IdvResource>(getResources()); 115 for (IdvResource resource : resources) { 116 ResourceCollection rc = getResources(resource); 117 logger.trace("Resource ID='{}'", resource); 118 for (int i = 0; i < rc.size(); i++) { 119 String path = (String)rc.get(i); 120 logger.trace(" path='{}' pathexists={}", path, isPathValid(path)); 121 } 122 } 123 } 124 125 /** 126 * Pretty much relies upon {@link IOUtil#getInputStream(String, Class)} 127 * to determine if {@code path} exists. 128 * 129 * @param path Path to an arbitrary file. It can be a remote URL, normal 130 * file on disk, or a file included in a JAR. Just so long as it's not 131 * {@code null}! 132 * 133 * @return {@code true} <i>iff</i> there were no problems. {@code false} 134 * otherwise. 135 */ 136 private boolean isPathValid(final String path) { 137 InputStream s = null; 138 boolean isValid = false; 139 try { 140 s = IOUtil.getInputStream(path, getClass()); 141 isValid = (s != null); 142 } catch (IOException e) { 143 isValid = false; 144 } finally { 145 if (s != null) { 146 try { 147 s.close(); 148 } catch (IOException e) { 149 logger.trace("could not close InputStream associated with "+path, e); 150 } 151 } 152 } 153 return isValid; 154 } 155 156 /** 157 * Adds support for McIDAS-V macros. Specifically: 158 * <ul> 159 * <li>{@link Constants#MACRO_VERSION}</li> 160 * </ul> 161 * 162 * @param path Path that contains a macro to be translated. 163 * 164 * @return Resource with our macros applied. 165 * 166 * @see IdvResourceManager#getResourcePath(String) 167 */ 168 @Override public String getResourcePath(String path) { 169 String retPath = path; 170 if (path.contains(Constants.MACRO_VERSION)) { 171 retPath = StringUtil.replace( 172 path, 173 Constants.MACRO_VERSION, 174 ((edu.wisc.ssec.mcidasv.StateManager)getStateManager()).getMcIdasVersion()); 175 } else { 176 retPath = super.getResourcePath(path); 177 } 178 return retPath; 179 } 180 181 /** 182 * Look for existing "default.mcv" and "default.xidv" bundles in root userpath 183 * If they exist, move them to the "bundles" directory, preferring "default.mcv" 184 */ 185 private void checkMoveOutdatedDefaultBundle() { 186 String userDirectory = getIdv().getObjectStore().getUserDirectory().toString(); 187 188 File defaultDir; 189 File defaultNew; 190 File defaultIdv; 191 File defaultMcv; 192 193 String os = System.getProperty("os.name"); 194 if (os == null) { 195 throw new RuntimeException(); 196 } 197 198 if (os.startsWith("Windows")) { 199 defaultDir = new File(userDirectory + "\\bundles\\General"); 200 defaultNew = new File(defaultDir.toString() + "\\Default.mcv"); 201 defaultIdv = new File(userDirectory + "\\default.xidv"); 202 defaultMcv = new File(userDirectory + "\\default.mcv"); 203 } else { 204 defaultDir = new File(userDirectory + "/bundles/General"); 205 defaultNew = new File(defaultDir.toString() + "/Default.mcv"); 206 defaultIdv = new File(userDirectory + "/default.xidv"); 207 defaultMcv = new File(userDirectory + "/default.mcv"); 208 } 209 210 // If no Alpha default bundles exist, bail quickly 211 if (!defaultIdv.exists() && !defaultMcv.exists()) { 212 return; 213 } 214 215 // If the destination directory does not exist, create it. 216 if (!defaultDir.exists()) { 217 if (!defaultDir.mkdirs()) { 218 logger.warn("Cannot create directory '{}' for default bundle", defaultDir); 219 return; 220 } 221 } 222 223 // If the destination already exists, print lame error message and bail. 224 // This whole check should only happen with Alphas so no biggie right? 225 if (defaultNew.exists()) { 226 logger.warn("Cannot copy current default bundle: '{}' already exists.", defaultNew); 227 return; 228 } 229 230 // If only default.xidv exists, try to rename it. 231 // If both exist, delete the default.xidv file. It was being ignored anyway. 232 if (defaultIdv.exists()) { 233 if (defaultMcv.exists()) { 234 defaultIdv.delete(); 235 } else { 236 if (!defaultIdv.renameTo(defaultNew)) { 237 logger.warn("Cannot copy current default bundle: error renaming '{}'", defaultIdv); 238 } 239 } 240 } 241 242 // If only default.mcv exists, try to rename it. 243 if (defaultMcv.exists()) { 244 if (!defaultMcv.renameTo(defaultNew)) { 245 logger.warn("Cannot copy current default bundle: error renaming '{}'", defaultMcv); 246 } 247 } 248 } 249 250 /** 251 * Checks an individual map resource (typically from {@code RSC_MAPS}) to 252 * verify that all of the specified maps exist? 253 * 254 * <p>Currently a no-op. The intention is to return a {@code List} so that the 255 * set of missing resources can eventually be sent off in a support 256 * request... 257 * 258 * <p>We could also decide to allow the user to search the list of plugins 259 * or ignore any missing resources (simply remove the bad stuff from the list of available xml). 260 * 261 * @param path Path to a map resource. URLs are allowed, but {@code null} is not. 262 * 263 * @return List of map paths that could not be read. If there were no 264 * errors the list is merely empty. 265 * 266 * @see IdvResourceManager#RSC_MAPS 267 */ 268 private List<String> getInvalidMapsInResource(final String path) { 269 List<String> invalidMaps = new ArrayList<String>(); 270 return invalidMaps; 271 } 272 273 /** 274 * Returns either a {@literal "normal"} {@link ResourceCollection} or a 275 * {@link ucar.unidata.xml.XmlResourceCollection XmlResourceCollection}, 276 * based upon {@code rsrc}. 277 * 278 * @param rsrc XML representation of a resource collection. Should not be 279 * {@code null}. 280 * @param name {@literal "name"} to associate with the returned 281 * {@code ResourceCollection}. Should not be {@code null}. 282 * 283 * @return {@code ResourceCollection} represented by {@code rsrc}. 284 */ 285 private ResourceCollection getCollection(final Element rsrc, final String name) { 286 ResourceCollection rc = getResources(name); 287 if (rc != null) { 288 return rc; 289 } 290 291 if (getAttribute(rsrc, ATTR_RESOURCETYPE, "text").equals("text")) { 292 return createResourceCollection(name); 293 } else { 294 return createXmlResourceCollection(name); 295 } 296 } 297 298 /** 299 * {@literal "Resource"} elements within a RBI file are allowed to have an 300 * arbitrary number of {@literal "property"} child elements (or none at 301 * all). The property elements must have {@literal "name"} and 302 * {@literal "value"} attributes. 303 * 304 * <p>This method iterates through any property elements and creates a {@link Map} 305 * of {@code name:value} pairs. 306 * 307 * @param resourceNode The {@literal "resource"} element to examine. Should 308 * not be {@code null}. Resources without {@code property}s are permitted. 309 * 310 * @return Either a {@code Map} of {@code name:value} pairs or an empty 311 * {@code Map}. 312 */ 313 private Map<String, String> getNodeProperties(final Element resourceNode) { 314 NodeList propertyList = getElements(resourceNode, TAG_PROPERTY); 315 Map<String, String> nodeProperties = new LinkedHashMap<String, String>(propertyList.getLength()); 316 for (int propIdx = 0; propIdx < propertyList.getLength(); propIdx++) { 317 Element propNode = (Element)propertyList.item(propIdx); 318 String propName = getAttribute(propNode, ATTR_NAME); 319 String propValue = getAttribute(propNode, ATTR_VALUE, (String)null); 320 if (propValue == null) { 321 propValue = getChildText(propNode); 322 } 323 nodeProperties.put(propName, propValue); 324 } 325 nodeProperties.putAll(getNodeAttributes(resourceNode)); 326 return nodeProperties; 327 } 328 329 /** 330 * Builds an {@code attribute:value} {@link Map} based upon the contents of 331 * {@code resourceNode}. 332 * 333 * <p><b>Be aware</b> that {@literal "location"} and {@literal "id"} attributes 334 * are ignored, as the IDV apparently considers them to be special. 335 * 336 * @param resourceNode The XML element to examine. Should not be 337 * {@code null}. 338 * 339 * @return Either a {@code Map} of {@code attribute:value} pairs or an 340 * empty {@code Map}. 341 */ 342 private Map<String, String> getNodeAttributes(final Element resourceNode) { 343 Map<String, String> nodeProperties = Collections.emptyMap(); 344 NamedNodeMap nnm = resourceNode.getAttributes(); 345 if (nnm != null) { 346 nodeProperties = new LinkedHashMap<String, String>(nnm.getLength()); 347 for (int attrIdx = 0; attrIdx < nnm.getLength(); attrIdx++) { 348 Attr attr = (Attr)nnm.item(attrIdx); 349 String name = attr.getNodeName(); 350 if (!name.equals(ATTR_LOCATION) && !name.equals(ATTR_ID)) { 351 nodeProperties.put(name, attr.getNodeValue()); 352 } 353 } 354 } 355 return nodeProperties; 356 } 357 358 /** 359 * Expands {@code origPath} (if needed) and builds a {@link List} of paths. 360 * Paths beginning with {@literal "index:"} or {@literal "http:"} may be in 361 * need of expansion. 362 * 363 * <p>{@literal "Index"} files contain a list of paths. These paths should 364 * be used instead of {@code origPath}. 365 * 366 * <p>Files that reside on a webserver (these begin with {@literal "http:"}) 367 * may be inaccessible for a variety of reasons. McIDAS-V allows a RBI file 368 * to specify a {@literal "property"} named {@literal "default"} whose 369 * {@literal "value"} is a path to use as a backup. 370 * 371 * <p>For example: 372 * 373 * <pre> 374 * <resources name="idv.resource.pluginindex"> 375 * <resource label="Plugin Index" location="https://www.ssec.wisc.edu/mcidas/software/v/resources/plugins/plugins.xml"> 376 * <property name="default" value="%APPPATH%/plugins.xml"/> 377 * </resource> 378 * </resources> 379 * </pre> 380 * 381 * The {@code origPath} parameter will be the value of the 382 * {@literal "location"} attribute. If {@code origPath} is inaccessible, 383 * then the path given by the {@literal "default"} property will be used. 384 * 385 * @param origPath Typically the value of the {@literal "location"} 386 * attribute associated with a given resource. Cannot be {@code null}. 387 * @param props Contains the property {@code name:value} pairs associated 388 * with the resource whose path is being examined. Cannot be {@code null}. 389 * 390 * @return {@code List} of paths associated with a given resource. 391 * 392 * @see #isPathValid(String) 393 */ 394 private List<String> getPaths(final String origPath, 395 final Map<String, String> props) 396 { 397 List<String> paths = new ArrayList<String>(); 398 if (origPath.startsWith("index:")) { 399 String path = origPath.substring(6); 400 String index = IOUtil.readContents(path, (String)null); 401 if (index != null) { 402 List<String> lines = StringUtil.split(index, "\n", true, true); 403 for (int lineIdx = 0; lineIdx < lines.size(); lineIdx++) { 404 String line = lines.get(lineIdx); 405 if (line.startsWith("#")) { 406 continue; 407 } 408 paths.add(getResourcePath(line)); 409 } 410 } 411 } else if (origPath.startsWith("http:")) { 412 String tmpPath = origPath; 413 if (!isPathValid(tmpPath) && props.containsKey("default")) { 414 tmpPath = getResourcePath(props.get("default")); 415 } 416 paths.add(tmpPath); 417 } else { 418 paths.add(origPath); 419 } 420 return paths; 421 } 422 423 /** 424 * Utility method that calls {@link StateManager#fixIds(String)}. 425 * 426 * @param resource Resource whose ID should be fixed. 427 * 428 * @return {@literal "Fixed"} ID for {@code resource}. 429 * 430 * @see StateManager#fixIds(String) 431 */ 432 private static String fixId(final Element resource) { 433 return StateManager.fixIds(getAttribute(resource, ATTR_NAME)); 434 } 435 436 /** 437 * Processes the top-level {@literal "root"} of a RBI XML file. Overridden 438 * in McIDAS-V so that remote resources can have a backup location. 439 * 440 * @param root The {@literal "root"} element. Should not be {@code null}. 441 * @param observeLoadMore Whether or not processing should continue if a 442 * {@literal "loadmore"} tag is encountered. 443 * 444 * @see #getPaths(String, Map) 445 */ 446 @Override protected void processRbi(final Element root, 447 final boolean observeLoadMore) 448 { 449 NodeList children = getElements(root, TAG_RESOURCES); 450 451 for (int i = 0; i < children.getLength(); i++) { 452 Element rsrc = (Element)children.item(i); 453 454 ResourceCollection rc = getCollection(rsrc, fixId(rsrc)); 455 if (getAttribute(rsrc, ATTR_REMOVEPREVIOUS, false)) { 456 rc.removeAll(); 457 } 458 459 if (observeLoadMore && !rc.getCanLoadMore()) { 460 continue; 461 } 462 463 boolean loadMore = getAttribute(rsrc, ATTR_LOADMORE, true); 464 if (!loadMore) { 465 rc.setCanLoadMore(false); 466 } 467 468 List<Resource> locationList = new ArrayList<Resource>(); 469 NodeList resources = getElements(rsrc, TAG_RESOURCE); 470 for (int idx = 0; idx < resources.getLength(); idx++) { 471 Element node = (Element)resources.item(idx); 472 String path = getResourcePath(getAttribute(node, ATTR_LOCATION)); 473 if ((path == null) || (path.isEmpty())) { 474 continue; 475 } 476 477 String label = getAttribute(node, ATTR_LABEL, (String)null); 478 String id = getAttribute(node, ATTR_ID, (String)null); 479 480 Map<String, String> nodeProperties = getNodeProperties(node); 481 482 for (String p : getPaths(path, nodeProperties)) { 483 if (id != null) { 484 rc.setIdForPath(id, p); 485 } 486 locationList.add(new Resource(p, label, new Hashtable<String, String>(nodeProperties))); 487 } 488 } 489 rc.addResources(locationList); 490 } 491 492 } 493}