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.servermanager;
030    
031    import static ucar.unidata.xml.XmlUtil.findChildren;
032    import static ucar.unidata.xml.XmlUtil.getAttribute;
033    
034    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
035    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.cast;
036    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.map;
037    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashSet;
038    import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newMap;
039    
040    import java.io.BufferedReader;
041    import java.io.BufferedWriter;
042    import java.io.FileReader;
043    import java.io.FileWriter;
044    import java.io.IOException;
045    import java.io.InputStream;
046    import java.io.InputStreamReader;
047    import java.util.Collection;
048    import java.util.Collections;
049    import java.util.EnumSet;
050    import java.util.HashMap;
051    import java.util.HashSet;
052    import java.util.List;
053    import java.util.Map;
054    import java.util.Set;
055    import java.util.Map.Entry;
056    import java.util.regex.Matcher;
057    import java.util.regex.Pattern;
058    
059    import org.slf4j.Logger;
060    import org.slf4j.LoggerFactory;
061    import org.w3c.dom.Element;
062    
063    import ucar.unidata.idv.IdvResourceManager;
064    import ucar.unidata.idv.chooser.adde.AddeServer;
065    import ucar.unidata.idv.chooser.adde.AddeServer.Group;
066    import ucar.unidata.util.IOUtil;
067    import ucar.unidata.util.LogUtil;
068    import ucar.unidata.util.StringUtil;
069    
070    import edu.wisc.ssec.mcidasv.ResourceManager;
071    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
072    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
073    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
074    import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
075    import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.AddeFormat;
076    import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.ServerName;
077    import edu.wisc.ssec.mcidasv.util.Contract;
078    import edu.wisc.ssec.mcidasv.util.functional.Function;
079    
080    /**
081     * Useful methods for doing things like converting a 
082     * {@link ucar.unidata.idv.chooser.adde.AddeServer AddeServer} to a
083     * {@link edu.wisc.ssec.mcidasv.servermanager.RemoteAddeEntry RemoteAddeEntry}.
084     */
085    public class EntryTransforms {
086    
087        /** Logger object. */
088        private static final Logger logger = LoggerFactory.getLogger(EntryTransforms.class);
089    
090        /** Matches dataset routing information in a MCTABLE file. */
091        private static final Pattern routePattern = 
092            Pattern.compile("^ADDE_ROUTE_(.*)=(.*)$");
093    
094        /** Matches {@literal "host"} declarations in a MCTABLE file. */
095        private static final Pattern hostPattern = 
096            Pattern.compile("^HOST_(.*)=(.*)$");
097    
098        /** No sense in rebuilding things that don't need to be rebuilt. */
099        private static final Matcher routeMatcher = routePattern.matcher("");
100    
101        /** No sense in rebuilding things that don't need to be rebuilt. */
102        private static final Matcher hostMatcher = hostPattern.matcher("");
103    
104        // TODO(jon): plz to be removing these
105        private static final String cygwinPrefix = "/cygdrive/";
106        private static final int cygwinPrefixLength = cygwinPrefix.length();
107    
108        /** This is a utility class. Don't create it! */
109        private EntryTransforms() { }
110    
111        /**
112         * {@link Function} that transforms an {@link AddeServer} into a {@link RemoteAddeEntry}.
113         */
114        // TODO(jon): shouldn't this use AddeEntry rather than RemoteAddeEntry?
115        public static final Function<AddeServer, RemoteAddeEntry> convertIdvServer = new Function<AddeServer, RemoteAddeEntry>() {
116            public RemoteAddeEntry apply(final AddeServer arg) {
117                String hostname = arg.toString().toLowerCase();
118                for (AddeServer.Group group : (List<AddeServer.Group>)arg.getGroups()) {
119                    
120                }
121                return new RemoteAddeEntry.Builder(hostname, "temp").build();
122            }
123        };
124    
125        @SuppressWarnings({"SetReplaceableByEnumSet"})
126        public static Set<EntryType> findEntryTypes(final Collection<? extends AddeEntry> entries) {
127            Set<EntryType> types = new HashSet<EntryType>(entries.size());
128            for (AddeEntry entry : entries) {
129                types.add(entry.getEntryType());
130            }
131            return EnumSet.copyOf(types);
132        }
133    
134        // converts a list of AddeServers to a set of RemoteAddeEntry
135        public static Set<RemoteAddeEntry> convertIdvServers(final List<AddeServer> idvServers) {
136            Set<RemoteAddeEntry> addeEntries = newLinkedHashSet();
137            addeEntries.addAll(map(convertIdvServer, idvServers));
138            return addeEntries;
139        }
140    
141        public static Set<AddeServer> convertMcvServers(final Collection<AddeEntry> entries) {
142            Set<AddeServer> addeServs = newLinkedHashSet(entries.size());
143            Set<String> addrs = newLinkedHashSet(entries.size());
144            for (AddeEntry e : entries) {
145                EntryStatus status = e.getEntryStatus();
146                if (status == EntryStatus.DISABLED || status == EntryStatus.INVALID) {
147                    continue;
148                }
149                String addr = e.getAddress();
150                if (addrs.contains(addr)) {
151                    continue;
152                }
153    
154                String newGroup = e.getGroup();
155                String type = entryTypeToStr(e.getEntryType());
156    
157                AddeServer addeServ;
158                if (e instanceof LocalAddeEntry) {
159                    addeServ = new AddeServer("localhost:"+EntryStore.getLocalPort(), "<LOCAL-DATA>");
160                    addeServ.setIsLocal(true);
161                } else {
162                    addeServ = new AddeServer(addr);
163                }
164                Group addeGroup = new Group(type, newGroup, newGroup);
165                addeServ.addGroup(addeGroup);
166                addeServs.add(addeServ);
167                addrs.add(addr);
168            }
169            return addeServs;
170        }
171    
172        /**
173         * Converts the XML contents of {@link ResourceManager#RSC_NEW_USERSERVERS}
174         * to a {@link Set} of {@link RemoteAddeEntry}s.
175         * 
176         * @param root {@literal "Root"} of the XML to convert.
177         * 
178         * @return {@code Set} of {@code RemoteAddeEntry}s described by 
179         * {@code root}.
180         */
181        protected static Set<RemoteAddeEntry> convertUserXml(final Element root) {
182            // <entry name="SERVER/DATASET" user="ASDF" proj="0000" source="user" enabled="true" type="image"/>
183            Pattern slashSplit = Pattern.compile("/");
184            List<Element> elements = cast(findChildren(root, "entry"));
185            Set<RemoteAddeEntry> entries = newLinkedHashSet(elements.size());
186            for (Element entryXml : elements) {
187                String name = getAttribute(entryXml, "name");
188                String user = getAttribute(entryXml, "user");
189                String proj = getAttribute(entryXml, "proj");
190                String source = getAttribute(entryXml, "source");
191                String type = getAttribute(entryXml, "type");
192    
193                boolean enabled = Boolean.parseBoolean(getAttribute(entryXml, "enabled"));
194    
195                EntryType entryType = strToEntryType(type);
196                EntryStatus entryStatus = (enabled) ? EntryStatus.ENABLED : EntryStatus.DISABLED;
197                EntrySource entrySource = strToEntrySource(source);
198    
199                if (name != null) {
200                    String[] arr = slashSplit.split(name);
201                    String description = arr[0];
202                    if (arr[0].toLowerCase().contains("localhost")) {
203                        description = "<LOCAL-DATA>";
204                    }
205    
206                    RemoteAddeEntry.Builder incomplete = 
207                        new RemoteAddeEntry.Builder(arr[0], arr[1])
208                            .type(entryType)
209                            .status(entryStatus)
210                            .source(entrySource)
211                            .validity(EntryValidity.VERIFIED);
212    
213                    if (((user != null) && (proj != null)) && ((!user.isEmpty()) && (!proj.isEmpty()))) {
214                        incomplete = incomplete.account(user, proj);
215                    }
216                    entries.add(incomplete.build());
217                }
218            }
219            return entries;
220        }
221    
222        public static Set<RemoteAddeEntry> createEntriesFrom(final RemoteAddeEntry entry) {
223            Set<RemoteAddeEntry> entries = newLinkedHashSet(EntryType.values().length);
224            RemoteAddeEntry.Builder incomp = 
225                new RemoteAddeEntry.Builder(entry.getAddress(), entry.getGroup())
226                .account(entry.getAccount().getUsername(), entry.getAccount().getProject())
227                .source(entry.getEntrySource()).status(entry.getEntryStatus())
228                .validity(entry.getEntryValidity());
229            for (EntryType type : EnumSet.of(EntryType.IMAGE, EntryType.GRID, EntryType.POINT, EntryType.TEXT, EntryType.RADAR, EntryType.NAV)) {
230                if (!(type == entry.getEntryType())) {
231                    entries.add(incomp.type(type).build());
232                }
233            }
234            logger.trace("built entries={}", entries);
235            return entries;
236        }
237    
238        
239        /**
240         * Converts the XML contents of {@link IdvResourceManager#RSC_ADDESERVER} 
241         * to a {@link Set} of {@link RemoteAddeEntry}s.
242         * 
243         * @param root XML to convert.
244         * @param source Used to {@literal "bulk set"} the origin of whatever
245         * {@code RemoteAddeEntry}s get created.
246         * 
247         * @return {@code Set} of {@code RemoteAddeEntry}s contained within 
248         * {@code root}.
249         */
250        @SuppressWarnings("unchecked")
251        protected static Set<AddeEntry> convertAddeServerXml(Element root, EntrySource source) {
252            List<Element> serverNodes = findChildren(root, "server");
253            Set<AddeEntry> es = newLinkedHashSet(serverNodes.size() * 5);
254            for (int i = 0; i < serverNodes.size(); i++) {
255                Element element = serverNodes.get(i);
256                String address = getAttribute(element, "name");
257                String description = getAttribute(element, "description", "");
258    
259                // loop through each "group" entry.
260                List<Element> groupNodes = findChildren(element, "group");
261                for (int j = 0; j < groupNodes.size(); j++) {
262                    Element group = groupNodes.get(j);
263    
264                    // convert whatever came out of the "type" attribute into a 
265                    // valid EntryType.
266                    String strType = getAttribute(group, "type");
267                    EntryType type = strToEntryType(strType);
268    
269                    // the "names" attribute can contain comma-delimited group
270                    // names.
271                    List<String> names = StringUtil.split(getAttribute(group, "names", ""), ",", true, true);
272                    for (String name : names) {
273                        if (name.isEmpty()) {
274                            continue;
275                        }
276                        RemoteAddeEntry e =  new RemoteAddeEntry
277                                                .Builder(address, name)
278                                                .source(source)
279                                                .type(type)
280                                                .validity(EntryValidity.VERIFIED)
281                                                .status(EntryStatus.ENABLED)
282                                                .validity(EntryValidity.VERIFIED)
283                                                .status(EntryStatus.ENABLED)
284                                                .build();
285                        es.add(e);
286                    }
287    
288                    // there's also an optional "name" attribute! woo!
289                    String name = getAttribute(group, "name", (String) null);
290                    if ((name != null) && (!name.isEmpty())) {
291    
292                        RemoteAddeEntry e = new RemoteAddeEntry
293                                                .Builder(address, name)
294                                                .source(source)
295                                                .validity(EntryValidity.VERIFIED)
296                                                .status(EntryStatus.ENABLED)
297                                                .validity(EntryValidity.VERIFIED)
298                                                .status(EntryStatus.ENABLED)
299                                                .build();
300                        es.add(e);
301                    }
302                }
303            }
304            return es;
305        }
306    
307        /**
308         * Converts a given {@link ServerName} to its {@link String} representation.
309         * Note that the resulting {@code String} is lowercase.
310         * 
311         * @param serverName The server name to convert. Cannot be {@code null}.
312         * 
313         * @return {@code serverName} converted to a lowercase {@code String} representation.
314         * 
315         * @throws NullPointerException if {@code serverName} is {@code null}.
316         */
317        public static String serverNameToStr(final ServerName serverName) {
318            Contract.notNull(serverName);
319            return serverName.toString().toLowerCase();
320        }
321    
322        /**
323         * Attempts to convert a {@link String} to a {@link ServerName}.
324         * 
325         * @param s Value whose {@code ServerName} is wanted. Cannot be {@code null}.
326         * 
327         * @return One of {@code ServerName}. If there was no {@literal "sensible"}
328         * conversion, the method returns {@link ServerName#INVALID}.
329         * 
330         * @throws NullPointerException if {@code s} is {@code null}.
331         */
332        public static ServerName strToServerName(final String s) {
333            ServerName serverName = ServerName.INVALID;
334            Contract.notNull(s);
335            try {
336                serverName = ServerName.valueOf(s.toUpperCase());
337            } catch (IllegalArgumentException e) {
338                // TODO: anything to do in this situation?
339            }
340            return serverName;
341        }
342    
343        /**
344         * Converts a given {@link EntryType} to its {@link String} representation.
345         * Note that the resulting {@code String} is lowercase.
346         * 
347         * @param type The type to convert. Cannot be {@code null}.
348         * 
349         * @return {@code type} converted to a lowercase {@code String} representation.
350         * 
351         * @throws NullPointerException if {@code type} is {@code null}.
352         */
353        public static String entryTypeToStr(final EntryType type) {
354            Contract.notNull(type);
355            return type.toString().toLowerCase();
356        }
357    
358        /**
359         * Attempts to convert a {@link String} to a {@link EntryType}.
360         * 
361         * @param s Value whose {@code EntryType} is wanted. Cannot be {@code null}.
362         * 
363         * @return One of {@code EntryType}. If there was no {@literal "sensible"}
364         * conversion, the method returns {@link EntryType#UNKNOWN}.
365         * 
366         * @throws NullPointerException if {@code s} is {@code null}.
367         */
368        public static EntryType strToEntryType(final String s) {
369            EntryType type = EntryType.UNKNOWN;
370            Contract.notNull(s);
371            try {
372                type = EntryType.valueOf(s.toUpperCase());
373            } catch (IllegalArgumentException e) {
374                // TODO: anything to do in this situation?
375            }
376            return type;
377        }
378    
379        /**
380         * Attempts to convert a {@link String} to an {@link EntrySource}.
381         * 
382         * @param s {@code String} representation of an {@code EntrySource}. 
383         * Cannot be {@code null}.
384         * 
385         * @return Uses {@link EntrySource#valueOf(String)} to convert {@code s}
386         * to an {@code EntrySource} and returns. If no conversion was possible, 
387         * returns {@link EntrySource#USER}.
388         * 
389         * @throws NullPointerException if {@code s} is {@code null}.
390         */
391        public static EntrySource strToEntrySource(final String s) {
392            EntrySource source = EntrySource.USER;
393            Contract.notNull(s);
394            try {
395                source = EntrySource.valueOf(s.toUpperCase());
396            } catch (IllegalArgumentException e) {
397                // TODO: anything to do in this situation?
398            }
399            return source;
400        }
401    
402        /**
403         * Attempts to convert a {@link String} to an {@link EntryValidity}.
404         * 
405         * @param s {@code String} representation of an {@code EntryValidity}. 
406         * Cannot be {@code null}.
407         * 
408         * @return Uses {@link EntryValidity#valueOf(String)} to convert 
409         * {@code s} to an {@code EntryValidity} and returns. If no conversion 
410         * was possible, returns {@link EntryValidity#UNVERIFIED}.
411         * 
412         * @throws NullPointerException if {@code s} is {@code null}.
413         */
414        public static EntryValidity strToEntryValidity(final String s) {
415            EntryValidity valid = EntryValidity.UNVERIFIED;
416            Contract.notNull(s);
417            try {
418                valid = EntryValidity.valueOf(s.toUpperCase());
419            } catch (IllegalArgumentException e) {
420                // TODO: anything to do in this situation?
421            }
422            return valid;
423        }
424    
425        /**
426         * Attempts to convert a {@link String} into an {@link EntryStatus}.
427         * 
428         * @param s {@code String} representation of an {@code EntryStatus}. 
429         * Cannot be {@code null}.
430         * 
431         * @return Uses {@link EntryStatus#valueOf(String)} to convert {@code s}
432         * into an {@code EntryStatus} and returns. If no conversion was possible, 
433         * returns {@link EntryStatus#DISABLED}.
434         * 
435         * @throws NullPointerException if {@code s} is {@code null}.
436         */
437        public static EntryStatus strToEntryStatus(final String s) {
438            EntryStatus status = EntryStatus.DISABLED;
439            Contract.notNull(s);
440            try {
441                status = EntryStatus.valueOf(s.toUpperCase());
442            } catch (IllegalArgumentException e) {
443                // TODO: anything to do in this situation?
444            }
445            return status;
446        }
447    
448        /**
449         * Attempts to convert a {@link String} into a member of {@link AddeFormat}.
450         * This method does a little bit of magic with the incoming {@code String}:
451         * <ol>
452         *   <li>spaces are replaced with underscores</li>
453         *   <li>dashes ({@literal "-"}) are removed</li>
454         * </ol>
455         * This was done because older {@literal "RESOLV.SRV"} files permitted the
456         * {@literal "MCV"} key to contain spaces or dashes, and that doesn't play
457         * so well with Java's enums.
458         * 
459         * @param s {@code String} representation of an {@code AddeFormat}. Cannot 
460         * be {@code null}.
461         * 
462         * @return Uses {@link AddeFormat#valueOf(String)} to convert <i>the modified</i>
463         * {@code String} into an {@code AddeFormat} and returns. If no conversion
464         * was possible, returns {@link AddeFormat#INVALID}.
465         * 
466         * @throws NullPointerException if {@code s} is {@code null}.
467         */
468        public static AddeFormat strToAddeFormat(final String s) {
469            AddeFormat format = AddeFormat.INVALID;
470            Contract.notNull(s);
471            try {
472                format = AddeFormat.valueOf(s.toUpperCase().replace(' ', '_').replace("-", ""));
473            } catch (IllegalArgumentException e) {
474                // TODO: anything to do in this situation?
475            }
476            return format;
477        }
478    
479        public static String addeFormatToStr(final AddeFormat format) {
480            Contract.notNull(format);
481            return format.toString().toLowerCase();
482        }
483    
484        // TODO(jon): re-add verify flag?
485        protected static Set<RemoteAddeEntry> extractMctableEntries(final String path, final String username, final String project) {
486            Set<RemoteAddeEntry> entries = newLinkedHashSet();
487            try {
488                InputStream is = IOUtil.getInputStream(path);
489                BufferedReader reader = new BufferedReader(new InputStreamReader(is));
490                String line;
491    
492                Map<String, Set<String>> hosts = newMap();
493                Map<String, String> hostToIp = newMap();
494                Map<String, String> datasetToHost = newMap();
495    
496                // special case for an local ADDE entries.
497                Set<String> blah = newLinkedHashSet();
498                blah.add("LOCAL-DATA");
499                hosts.put("LOCAL-DATA", blah);
500                hostToIp.put("LOCAL-DATA", "LOCAL-DATA");
501    
502                boolean validFile = false;
503                while ((line = reader.readLine()) != null) {
504                    routeMatcher.reset(line);
505                    hostMatcher.reset(line);
506    
507                    if (routeMatcher.find()) {
508                        String dataset = routeMatcher.group(1);
509                        String host = routeMatcher.group(2).toLowerCase();
510                        datasetToHost.put(dataset, host);
511                        validFile = true;
512                    }
513                    else if (hostMatcher.find()) {
514                        String name = hostMatcher.group(1).toLowerCase();
515                        String ip = hostMatcher.group(2);
516    
517                        Set<String> nameSet = hosts.get(ip);
518                        if (nameSet == null) {
519                            nameSet = newLinkedHashSet();
520                        }
521                        nameSet.add(name);
522                        hosts.put(ip, nameSet);
523                        hostToIp.put(name, ip);
524                        hostToIp.put(ip, ip); // HACK :(
525                        validFile = true;
526                    }
527                }
528    
529                if (validFile) {
530                    Map<String, String> datasetsToIp = mapDatasetsToIp(datasetToHost, hostToIp);
531                    Map<String, String> ipToName = mapIpToName(hosts);
532                    List<RemoteAddeEntry> l = mapDatasetsToName(datasetsToIp, ipToName, username, project);
533                    entries.addAll(l);
534                } else {
535                    entries = Collections.emptySet();
536                }
537                is.close();
538            } catch (IOException e) {
539                LogUtil.logException("Reading file: "+path, e);
540            }
541    
542            return entries;
543        }
544    
545        /**
546         * This method is slightly confusing, sorry! Think of it kind of like a
547         * {@literal "SQL JOIN"}... 
548         * 
549         * <p>Basically create {@link RemoteAddeEntry}s by using a hostname to
550         * determine which dataset belongs to which IP.
551         * 
552         * @param datasetToHost {@code Map} of ADDE groups to host names.
553         * @param hostToIp {@code Map} of host names to IP addresses.
554         * @param username ADDE username.
555         * @param project ADDE project number (as a {@code String}).
556         * 
557         * @return {@link List} of {@link RemoteAddeEntry} instances. Each hostname
558         * will have a value from {@code datasetToHost} and the accounting information
559         * is formed from {@code username} and {@code project}.
560         */
561        private static List<RemoteAddeEntry> mapDatasetsToName(
562            final Map<String, String> datasetToHost, final Map<String, String> hostToIp, final String username, final String project) 
563        {
564            boolean defaultAcct = false;
565            AddeAccount defAcct = AddeEntry.DEFAULT_ACCOUNT;
566            if (defAcct.getUsername().equalsIgnoreCase(username) && defAcct.getProject().equals(project)) {
567                defaultAcct = true;
568            }
569            List<RemoteAddeEntry> entries = arrList(datasetToHost.size());
570            for (Entry<String, String> entry : datasetToHost.entrySet()) {
571                String dataset = entry.getKey();
572                String ip = entry.getValue();
573                String name = ip;
574                if (hostToIp.containsKey(ip)) {
575                    name = hostToIp.get(ip);
576                }
577                RemoteAddeEntry.Builder builder = new RemoteAddeEntry.Builder(name, dataset)
578                                                      .source(EntrySource.MCTABLE);
579                if (!defaultAcct) {
580                    builder.account(username, project);
581                }
582                RemoteAddeEntry remoteEntry = builder.build();
583                logger.trace("built entry={}", remoteEntry);
584                entries.add(builder.build());
585            }
586            return entries;
587        }
588    
589        private static Map<String, String> mapIpToName(
590            final Map<String, Set<String>> map) 
591        {
592            assert map != null;
593    
594            Map<String, String> ipToName = newMap(map.size());
595            for (Entry<String, Set<String>> entry : map.entrySet()) {
596                Set<String> names = entry.getValue();
597                String displayName = "";
598                for (String name : names)
599                    if (name.length() >= displayName.length())
600                        displayName = name;
601    
602                if (displayName.isEmpty()) {
603                    displayName = entry.getKey();
604                }
605                ipToName.put(entry.getKey(), displayName);
606            }
607            return ipToName;
608        }
609    
610        private static Map<String, String> mapDatasetsToIp(final Map<String, String> datasets, final Map<String, String> hostMap) {
611            assert datasets != null;
612            assert hostMap != null;
613    
614            Map<String, String> datasetToIp = newMap(datasets.size());
615            for (Entry<String, String> entry : datasets.entrySet()) {
616                String dataset = entry.getKey();
617                String alias = entry.getValue();
618                if (hostMap.containsKey(alias)) {
619                    datasetToIp.put(dataset, hostMap.get(alias));
620                }
621            }
622            return datasetToIp;
623        }
624    
625        /**
626         * Reads a {@literal "RESOLV.SRV"} file and converts the contents into a 
627         * {@link Set} of {@link LocalAddeEntry}s.
628         * 
629         * @param filename Filename containing desired {@code LocalAddeEntry}s. 
630         * Cannot be {@code null}.
631         * 
632         * @return {@code Set} of {@code LocalAddeEntry}s contained within 
633         * {@code filename}.
634         * 
635         * @throws IOException if there was a problem reading from {@code filename}.
636         * 
637         * @see #readResolvLine(String)
638         */
639        public static Set<LocalAddeEntry> readResolvFile(final String filename) throws IOException {
640            Set<LocalAddeEntry> servers = newLinkedHashSet();
641            BufferedReader br = null;
642            try {
643                br = new BufferedReader(new FileReader(filename));
644                String line;
645                while ((line = br.readLine()) != null) {
646                    line = line.trim();
647                    if (line.isEmpty()) {
648                        continue;
649                    } else if (line.startsWith("SSH_")) {
650                        continue;
651                    }
652                    servers.add(readResolvLine(line));
653                }
654            } finally {
655                if (br != null) {
656                    br.close();
657                }
658            }
659            return servers;
660        }
661    
662        /**
663         * Converts a {@code String} containing a {@literal "RESOLV.SRV"} entry into
664         * a {@link LocalAddeEntry}.
665         */
666        public static LocalAddeEntry readResolvLine(String line) {
667            boolean disabled = line.startsWith("#");
668            if (disabled) {
669                line = line.substring(1);
670            }
671            Pattern commaSplit = Pattern.compile(",");
672            Pattern equalSplit = Pattern.compile("=");
673    
674            String[] pairs = commaSplit.split(line.trim());
675            String[] pair;
676            Map<String, String> keyVals = new HashMap<String, String>(pairs.length);
677            for (int i = 0; i < pairs.length; i++) {
678                if (pairs[i] == null || pairs[i].isEmpty()) {
679                    continue;
680                }
681    
682                pair = equalSplit.split(pairs[i]);
683                if (pair.length != 2 || pair[0].isEmpty() || pair[1].isEmpty()) {
684                    continue;
685                }
686    
687                // group
688    //            if ("N1".equals(pair[0])) {
689    ////                builder.group(pair[1]);
690    //            }
691    //            // descriptor/dataset
692    //            else if ("N2".equals(pair[0])) {
693    ////                builder.descriptor(pair[1]);
694    //            }
695    //            // data type (only image supported?)
696    //            else if ("TYPE".equals(pair[0])) {
697    ////                builder.type(strToEntryType(pair[1]));
698    //            }
699    //            // file format
700    //            else if ("K".equals(pair[0])) {
701    ////                builder.kind(pair[1].toUpperCase());
702    //            }
703    //            // comment
704    //            else if ("C".equals(pair[0])) {
705    ////                builder.name(pair[1]);
706    //            }
707    //            // mcv-specific; allows us to infer kind+type?
708    //            else if ("MCV".equals(pair[0])) {
709    ////                builder.format(strToAddeFormat(pair[1]));
710    //            }
711    //            // realtime ("Y"/"N"/"A")
712    //            else if ("RT".equals(pair[0])) {
713    ////                builder.realtime(pair[1]);
714    //            }
715    //            // start of file number range
716    //            else if ("R1".equals(pair[0])) {
717    ////                builder.start(pair[1]);
718    //            }
719    //            // end of file number range
720    //            else if ("R2".equals(pair[0])) {
721    ////                builder.end(pair[1]);
722    //            }
723    //            // filename mask
724                if ("MASK".equals(pair[0])) {
725                    pair[1] = demungeFileMask(pair[1]);
726                }
727                keyVals.put(pair[0], pair[1]);
728            }
729    
730            if (keyVals.containsKey("C") && keyVals.containsKey("N1") && keyVals.containsKey("MCV") && keyVals.containsKey("MASK")) {
731                LocalAddeEntry entry = new LocalAddeEntry.Builder(keyVals).build();
732                EntryStatus status = (disabled) ? EntryStatus.DISABLED : EntryStatus.ENABLED;
733                entry.setEntryStatus(status);
734                return entry;
735            } else {
736                return LocalAddeEntry.INVALID_ENTRY;
737            }
738        }
739    
740        /**
741         * Writes a {@link Collection} of {@link LocalAddeEntry}s to a {@literal "RESOLV.SRV"}
742         * file. <b>This method discards the current contents of {@code filename}!</b>
743         * 
744         * @param filename Filename that will contain the {@code LocalAddeEntry}s within 
745         * {@code entries}. Cannot be {@code null}.
746         * 
747         * @param entries {@code Set} of entries to be written to {@code filename}.
748         * Cannot be {@code null}.
749         * 
750         * @throws IOException if there was a problem writing to {@code filename}.
751         * 
752         * @see #appendResolvFile(String, Collection)
753         */
754        public static void writeResolvFile(final String filename, final Collection<LocalAddeEntry> entries) throws IOException {
755            writeResolvFile(filename, false, entries);
756        }
757    
758        /**
759         * Writes a {@link Collection} of {@link LocalAddeEntry}s to a {@literal "RESOLV.SRV"}
760         * file. This method will <i>append</i> the contents of {@code entries} to
761         * {@code filename}.
762         * 
763         * @param filename Filename that will contain the {@code LocalAddeEntry}s within 
764         * {@code entries}. Cannot be {@code null}.
765         * 
766         * @param entries {@code Collection} of entries to be written to {@code filename}.
767         * Cannot be {@code null}.
768         * 
769         * @throws IOException if there was a problem writing to {@code filename}.
770         * 
771         * @see #writeResolvFile(String, Collection)
772         */
773        public static void appendResolvFile(final String filename, final Collection<LocalAddeEntry> entries) throws IOException {
774            writeResolvFile(filename, true, entries);
775        }
776    
777        /**
778         * Writes a {@link Collection} of {@link LocalAddeEntry}s to a {@literal "RESOLV.SRV"}
779         * file.
780         * 
781         * @param filename Filename that will contain the {@code LocalAddeEntry}s within 
782         * {@code entries}. Cannot be {@code null}.
783         * 
784         * @param append If {@code true}, append {@code entries} to {@code filename}. Otherwise discards contents of {@code filename}.
785         * 
786         * @param entries {@code Collection} of entries to be written to {@code filename}.
787         * Cannot be {@code null}. 
788         * 
789         * @throws IOException if there was a problem writing to {@code filename}.
790         * 
791         * @see #appendResolvFile(String, Collection)
792         * @see #asResolvEntry(LocalAddeEntry)
793         */
794        private static void writeResolvFile(final String filename, final boolean append, final Collection<LocalAddeEntry> entries) throws IOException {
795            BufferedWriter bw = null;
796            try {
797                bw = new BufferedWriter(new FileWriter(filename));
798                for (LocalAddeEntry entry : entries) {
799                    bw.write(asResolvEntry(entry)+'\n');
800                }
801            } finally {
802                if (bw != null) {
803                    bw.close();
804                }
805            }
806        }
807    
808        public static Set<LocalAddeEntry> removeTemporaryEntriesFromResolvFile(final String filename, final Collection<LocalAddeEntry> entries) throws IOException {
809            Contract.notNull(filename, "Path to resolv file cannot be null");
810            Contract.notNull(entries, "Local entries cannot be null");
811            Set<LocalAddeEntry> removedEntries = newLinkedHashSet(entries.size());
812            BufferedWriter bw = null;
813            try {
814                bw = new BufferedWriter(new FileWriter(filename));
815                for (LocalAddeEntry entry : entries) {
816                    if (!entry.isEntryTemporary()) {
817                        bw.write(asResolvEntry(entry)+'\n');
818                    } else {
819                        removedEntries.add(entry);
820                    }
821                }
822            } finally {
823                if (bw != null) {
824                    bw.close();
825                }
826            }
827            return removedEntries;
828        }
829    
830        /**
831         * De-munges file mask strings.
832         * 
833         * @throws NullPointerException if {@code path} is {@code null}. 
834         */
835        public static String demungeFileMask(final String path) {
836            Contract.notNull(path, "how dare you! null paths cannot be munged!");
837            int index = path.indexOf("/*");
838            if (index < 0) {
839                return path;
840            }
841            String tmpFileMask = path.substring(0, index);
842            // Look for "cygwinPrefix" at start of string and munge accordingly
843            if (tmpFileMask.length() > cygwinPrefixLength+1 &&
844                tmpFileMask.substring(0,cygwinPrefixLength).equals(cygwinPrefix)) {
845                String driveLetter = tmpFileMask.substring(cygwinPrefixLength,cygwinPrefixLength+1).toUpperCase();
846                return driveLetter + ':' + tmpFileMask.substring(cygwinPrefixLength+1).replace('/', '\\');
847            } else {
848                return tmpFileMask;
849            }
850        }
851    
852        /**
853         * Munges a file mask {@link String} into something {@literal "RESOLV.SRV"}
854         * expects.
855         * 
856         * <p>Munging is only needed for Windows users--the process converts 
857         * back slashes into forward slashes and prefixes with {@literal "/cygdrive/"}.
858         * 
859         * @throws NullPointerException if {@code mask} is {@code null}.
860         */
861        public static String mungeFileMask(final String mask) {
862            Contract.notNull(mask, "Cannot further munge this mask; it was null upon arriving");
863            StringBuilder s = new StringBuilder(100);
864            if (mask.length() > 3 && ":".equals(mask.substring(1, 2))) {
865                String newFileMask = mask;
866                String driveLetter = newFileMask.substring(0, 1).toLowerCase();
867                newFileMask = newFileMask.substring(3);
868                newFileMask = newFileMask.replace('\\', '/');
869                s.append("/cygdrive/").append(driveLetter).append('/').append(newFileMask);
870            } else {
871                s.append("").append(mask);
872            }
873            return s.toString();
874        }
875    
876        /**
877         * Converts a {@link Collection} of {@link LocalAddeEntry}s into a {@link List}
878         * of {@code String}s. 
879         * 
880         * @param entries {@code Collection} of entries to convert. Should not be {@code null}.
881         * 
882         * @return {@code entries} represented as {@code String}s.
883         * 
884         * @see #asResolvEntry(LocalAddeEntry)
885         */
886        public static List<String> asResolvEntries(final Collection<LocalAddeEntry> entries) {
887            List<String> resolvEntries = arrList(entries.size());
888            for (LocalAddeEntry entry : entries) {
889                resolvEntries.add(asResolvEntry(entry));
890            }
891            return resolvEntries;
892        }
893    
894        /**
895         * Converts a given {@link LocalAddeEntry} into a {@code String} that is 
896         * suitable for including in a {@literal "RESOLV.SRV"} file. This method
897         * does <b>not</b> append a newline to the end of the {@code String}.
898         * 
899         * @param entry The {@code LocalAddeEntry} to convert. Should not be {@code null}.
900         * 
901         * @return {@code entry} as a {@literal "RESOLV.SRV"} entry.
902         */
903        public static String asResolvEntry(final LocalAddeEntry entry) {
904            AddeFormat format = entry.getFormat();
905            ServerName servName = format.getServerName();
906    
907            StringBuilder s = new StringBuilder(150);
908            if (entry.getEntryStatus() != EntryStatus.ENABLED) {
909                s.append('#');
910            }
911            s.append("N1=").append(entry.getGroup().toUpperCase())
912                .append(",N2=").append(entry.getDescriptor().toUpperCase())
913                .append(",TYPE=").append(format.getType())
914                .append(",RT=").append(entry.getRealtimeAsString())
915                .append(",K=").append(format.getServerName())
916                .append(",R1=").append(entry.getStart())
917                .append(",R2=").append(entry.getEnd())
918                .append(",MCV=").append(format.name())
919                .append(",C=").append(entry.getName())
920                .append(",TEMPORARY=").append(entry.isEntryTemporary());
921    
922            if (servName == ServerName.LV1B) {
923                s.append(",Q=LALO");
924            }
925    
926            String tmpFileMask = entry.getFileMask();
927            if (tmpFileMask.length() > 3 && ":".equals(tmpFileMask.substring(1, 2))) {
928                String newFileMask = tmpFileMask;
929                String driveLetter = newFileMask.substring(0, 1).toLowerCase();
930                newFileMask = newFileMask.substring(3);
931                newFileMask = newFileMask.replace('\\', '/');
932                s.append(",MASK=/cygdrive/").append(driveLetter).append('/').append(newFileMask);
933            } else {
934                s.append(",MASK=").append(tmpFileMask);
935            }
936            // local servers seem to really like trailing commas!
937            return s.append('/').append(format.getFileFilter()).append(',').toString(); 
938        }
939    }