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