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 }