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