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