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;
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 The {@literal "name"} to associate with the returned 
281     * {@code ResourceCollection}. Should not be {@code null}.
282     */
283    private ResourceCollection getCollection(final Element rsrc, final String name) {
284        ResourceCollection rc = getResources(name);
285        if (rc != null) {
286            return rc;
287        }
288        
289        if (getAttribute(rsrc, ATTR_RESOURCETYPE, "text").equals("text")) {
290            return createResourceCollection(name);
291        } else {
292            return createXmlResourceCollection(name);
293        }
294    }
295
296    /**
297     * {@literal "Resource"} elements within a RBI file are allowed to have an
298     * arbitrary number of {@literal "property"} child elements (or none at 
299     * all). The property elements must have {@literal "name"} and 
300     * {@literal "value"} attributes.
301     * 
302     * <p>This method iterates through any property elements and creates a {@link Map}
303     * of {@code name:value} pairs.
304     * 
305     * @param resourceNode The {@literal "resource"} element to examine. Should
306     * not be {@code null}. Resources without {@code property}s are permitted.
307     * 
308     * @return Either a {@code Map} of {@code name:value} pairs or an empty 
309     * {@code Map}. 
310     */
311    private Map<String, String> getNodeProperties(final Element resourceNode) {
312        NodeList propertyList = getElements(resourceNode, TAG_PROPERTY);
313        Map<String, String> nodeProperties = new LinkedHashMap<String, String>(propertyList.getLength());
314        for (int propIdx = 0; propIdx < propertyList.getLength(); propIdx++) {
315            Element propNode = (Element)propertyList.item(propIdx);
316            String propName = getAttribute(propNode, ATTR_NAME);
317            String propValue = getAttribute(propNode, ATTR_VALUE, (String)null);
318            if (propValue == null) {
319                propValue = getChildText(propNode);
320            }
321            nodeProperties.put(propName, propValue);
322        }
323        nodeProperties.putAll(getNodeAttributes(resourceNode));
324        return nodeProperties;
325    }
326
327    /**
328     * Builds an {@code attribute:value} {@link Map} based upon the contents of
329     * {@code resourceNode}.
330     * 
331     * <p><b>Be aware</b> that {@literal "location"} and {@literal "id"} attributes
332     * are ignored, as the IDV apparently considers them to be special.
333     * 
334     * @param resourceNode The XML element to examine. Should not be 
335     * {@code null}.
336     * 
337     * @return Either a {@code Map} of {@code attribute:value} pairs or an 
338     * empty {@code Map}.
339     */
340    private Map<String, String> getNodeAttributes(final Element resourceNode) {
341        Map<String, String> nodeProperties = Collections.emptyMap();
342        NamedNodeMap nnm = resourceNode.getAttributes();
343        if (nnm != null) {
344            nodeProperties = new LinkedHashMap<String, String>(nnm.getLength());
345            for (int attrIdx = 0; attrIdx < nnm.getLength(); attrIdx++) {
346                Attr attr = (Attr)nnm.item(attrIdx);
347                String name = attr.getNodeName();
348                if (!name.equals(ATTR_LOCATION) && !name.equals(ATTR_ID)) {
349                    nodeProperties.put(name, attr.getNodeValue());
350                }
351            }
352        }
353        return nodeProperties;
354    }
355
356    /**
357     * Expands {@code origPath} (if needed) and builds a {@link List} of paths.
358     * Paths beginning with {@literal "index:"} or {@literal "http:"} may be in
359     * need of expansion.
360     * 
361     * <p>{@literal "Index"} files contain a list of paths. These paths should
362     * be used instead of {@code origPath}.
363     * 
364     * <p>Files that reside on a webserver (these begin with {@literal "http:"})
365     * may be inaccessible for a variety of reasons. McIDAS-V allows a RBI file
366     * to specify a {@literal "property"} named {@literal "default"} whose {@literal "value"}
367     * is a path to use as a backup. For example:<br/>
368     * <pre>
369     * &lt;resources name="idv.resource.pluginindex"&gt;
370     *   &lt;resource label="Plugin Index" location="https://www.ssec.wisc.edu/mcidas/software/v/resources/plugins/plugins.xml"&gt;
371     *     &lt;property name="default" value="%APPPATH%/plugins.xml"/&gt;
372     *   &lt;/resource&gt;
373     * &lt;/resources&gt;
374     * </pre>
375     * The {@code origPath} parameter will be the value of the {@literal "location"}
376     * attribute. If {@code origPath} is inaccessible, then the path given by
377     * the {@literal "default"} property will be used.
378     * 
379     * @param origPath Typically the value of the {@literal "location"} 
380     * attribute associated with a given resource. Cannot be {@code null}.
381     * @param props Contains the property {@code name:value} pairs associated with
382     * the resource whose path is being examined. Cannot be {@code null}.
383     * 
384     * @return {@code List} of paths associated with a given resource.
385     * 
386     * @see #isPathValid(String)
387     */
388    private List<String> getPaths(final String origPath, 
389        final Map<String, String> props) 
390    {
391        List<String> paths = new ArrayList<String>();
392        if (origPath.startsWith("index:")) {
393            String path = origPath.substring(6);
394            String index = IOUtil.readContents(path, (String)null);
395            if (index != null) {
396                List<String> lines = StringUtil.split(index, "\n", true, true);
397                for (int lineIdx = 0; lineIdx < lines.size(); lineIdx++) {
398                    String line = lines.get(lineIdx);
399                    if (line.startsWith("#")) {
400                        continue;
401                    }
402                    paths.add(getResourcePath(line));
403                }
404            }
405        } else if (origPath.startsWith("http:")) {
406            String tmpPath = origPath;
407            if (!isPathValid(tmpPath) && props.containsKey("default")) {
408                tmpPath = getResourcePath(props.get("default"));
409            }
410            paths.add(tmpPath);
411        } else {
412            paths.add(origPath);
413        }
414        return paths;
415    }
416
417    /**
418     * Utility method that calls {@link StateManager#fixIds(String)}.
419     */
420    private static String fixId(final Element resource) {
421        return StateManager.fixIds(getAttribute(resource, ATTR_NAME));
422    }
423
424    /**
425     * Processes the top-level {@literal "root"} of a RBI XML file. Overridden
426     * in McIDAS-V so that remote resources can have a backup location.
427     * 
428     * @param root The {@literal "root"} element. Should not be {@code null}.
429     * @param observeLoadMore Whether or not processing should continue if a 
430     * {@literal "loadmore"} tag is encountered.
431     * 
432     * @see #getPaths(String, Map)
433     */
434    @Override protected void processRbi(final Element root, 
435        final boolean observeLoadMore) 
436    {
437        NodeList children = getElements(root, TAG_RESOURCES);
438
439        for (int i = 0; i < children.getLength(); i++) {
440            Element rsrc = (Element)children.item(i);
441
442            ResourceCollection rc = getCollection(rsrc, fixId(rsrc));
443            if (getAttribute(rsrc, ATTR_REMOVEPREVIOUS, false)) {
444                rc.removeAll();
445            }
446
447            if (observeLoadMore && !rc.getCanLoadMore()) {
448                continue;
449            }
450
451            boolean loadMore = getAttribute(rsrc, ATTR_LOADMORE, true);
452            if (!loadMore) {
453                rc.setCanLoadMore(false);
454            }
455
456            List<Resource> locationList = new ArrayList<Resource>();
457            NodeList resources = getElements(rsrc, TAG_RESOURCE);
458            for (int idx = 0; idx < resources.getLength(); idx++) {
459                Element node = (Element)resources.item(idx);
460                String path = getResourcePath(getAttribute(node, ATTR_LOCATION));
461                if ((path == null) || (path.isEmpty())) {
462                    continue;
463                }
464
465                String label = getAttribute(node, ATTR_LABEL, (String)null);
466                String id = getAttribute(node, ATTR_ID, (String)null);
467
468                Map<String, String> nodeProperties = getNodeProperties(node);
469
470                for (String p : getPaths(path, nodeProperties)) {
471                    if (id != null) {
472                        rc.setIdForPath(id, p);
473                    }
474                    locationList.add(new Resource(p, label, new Hashtable<String, String>(nodeProperties)));
475                }
476            }
477            rc.addResources(locationList);
478        }
479
480    }
481}