001    /*
002     * $Id: ResourceManager.java,v 1.15 2012/02/19 17:35:53 davep Exp $
003     *
004     * This file is part of McIDAS-V
005     *
006     * Copyright 2007-2012
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    
031    package edu.wisc.ssec.mcidasv;
032    
033    import static ucar.unidata.xml.XmlUtil.getAttribute;
034    import static ucar.unidata.xml.XmlUtil.getChildText;
035    import static ucar.unidata.xml.XmlUtil.getElements;
036    
037    import java.io.File;
038    import java.io.IOException;
039    import java.io.InputStream;
040    import java.util.ArrayList;
041    import java.util.Hashtable;
042    import java.util.LinkedHashMap;
043    import java.util.List;
044    import java.util.Map;
045    
046    import org.w3c.dom.Attr;
047    import org.w3c.dom.Element;
048    import org.w3c.dom.NamedNodeMap;
049    import org.w3c.dom.NodeList;
050    
051    import ucar.unidata.idv.IdvResourceManager;
052    import ucar.unidata.idv.IntegratedDataViewer;
053    import ucar.unidata.idv.StateManager;
054    import ucar.unidata.util.IOUtil;
055    import ucar.unidata.util.ResourceCollection;
056    import ucar.unidata.util.ResourceCollection.Resource;
057    import ucar.unidata.util.StringUtil;
058    
059    /**
060     * @author McIDAS-V Team
061     * @version $Id: ResourceManager.java,v 1.15 2012/02/19 17:35:53 davep Exp $
062     */
063    public 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    }