001/*
002 * This file is part of McIDAS-V
003 *
004 * Copyright 2007-2023
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     * &lt;resources name="idv.resource.pluginindex"&gt;
375     *   &lt;resource label="Plugin Index" location="https://www.ssec.wisc.edu/mcidas/software/v/resources/plugins/plugins.xml"&gt;
376     *     &lt;property name="default" value="%APPPATH%/plugins.xml"/&gt;
377     *   &lt;/resource&gt;
378     * &lt;/resources&gt;
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}