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         * &lt;resources name="idv.resource.pluginindex"&gt;
368         *   &lt;resource label="Plugin Index" location="https://www.ssec.wisc.edu/mcidas/software/v/resources/plugins/plugins.xml"&gt;
369         *     &lt;property name="default" value="%APPPATH%/plugins.xml"/&gt;
370         *   &lt;/resource&gt;
371         * &lt;/resources&gt;
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    }