001/*
002 * $Id: EntryTransforms.java,v 1.35 2011/04/06 20:05:33 jbeavers Exp $
003 *
004 * This file is part of McIDAS-V
005 *
006 * Copyright 2007-2011
007 * Space Science and Engineering Center (SSEC)
008 * University of Wisconsin - Madison
009 * 1225 W. Dayton Street, Madison, WI 53706, USA
010 * https://www.ssec.wisc.edu/mcidas
011 * 
012 * All Rights Reserved
013 * 
014 * McIDAS-V is built on Unidata's IDV and SSEC's VisAD libraries, and
015 * some McIDAS-V source code is based on IDV and VisAD source code.  
016 * 
017 * McIDAS-V is free software; you can redistribute it and/or modify
018 * it under the terms of the GNU Lesser Public License as published by
019 * the Free Software Foundation; either version 3 of the License, or
020 * (at your option) any later version.
021 * 
022 * McIDAS-V is distributed in the hope that it will be useful,
023 * but WITHOUT ANY WARRANTY; without even the implied warranty of
024 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
025 * GNU Lesser Public License for more details.
026 * 
027 * You should have received a copy of the GNU Lesser Public License
028 * along with this program.  If not, see http://www.gnu.org/licenses.
029 */
030
031package edu.wisc.ssec.mcidasv.servermanager;
032
033import static ucar.unidata.xml.XmlUtil.findChildren;
034import static ucar.unidata.xml.XmlUtil.getAttribute;
035
036import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList;
037import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.cast;
038import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.map;
039import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashSet;
040import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newMap;
041
042import java.io.BufferedReader;
043import java.io.BufferedWriter;
044import java.io.FileReader;
045import java.io.FileWriter;
046import java.io.IOException;
047import java.io.InputStream;
048import java.io.InputStreamReader;
049import java.util.Collection;
050import java.util.Collections;
051import java.util.EnumSet;
052import java.util.HashMap;
053import java.util.HashSet;
054import java.util.List;
055import java.util.Map;
056import java.util.Set;
057import java.util.Map.Entry;
058import java.util.regex.Matcher;
059import java.util.regex.Pattern;
060
061import org.slf4j.Logger;
062import org.slf4j.LoggerFactory;
063import org.w3c.dom.Element;
064
065import ucar.unidata.idv.IdvResourceManager;
066import ucar.unidata.idv.chooser.adde.AddeServer;
067import ucar.unidata.idv.chooser.adde.AddeServer.Group;
068import ucar.unidata.util.IOUtil;
069import ucar.unidata.util.LogUtil;
070import ucar.unidata.util.StringUtil;
071
072import edu.wisc.ssec.mcidasv.ResourceManager;
073import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntrySource;
074import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryStatus;
075import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryType;
076import edu.wisc.ssec.mcidasv.servermanager.AddeEntry.EntryValidity;
077import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.AddeFormat;
078import edu.wisc.ssec.mcidasv.servermanager.LocalAddeEntry.ServerName;
079import edu.wisc.ssec.mcidasv.util.Contract;
080import 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 */
087public 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
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
516     * @param project
517     * 
518     * @return
519     */
520    private static List<RemoteAddeEntry> mapDatasetsToName(
521        final Map<String, String> datasetToHost, final Map<String, String> hostToIp, final String username, final String project) 
522    {
523        boolean defaultAcct = false;
524        AddeAccount defAcct = AddeEntry.DEFAULT_ACCOUNT;
525        if (defAcct.getUsername().equalsIgnoreCase(username) && defAcct.getProject().equals(project)) {
526            defaultAcct = true;
527        }
528        List<RemoteAddeEntry> entries = arrList(datasetToHost.size());
529        for (Entry<String, String> entry : datasetToHost.entrySet()) {
530            String dataset = entry.getKey();
531            String ip = entry.getValue();
532            String name = ip;
533            if (hostToIp.containsKey(ip)) {
534                name = hostToIp.get(ip);
535            }
536            RemoteAddeEntry.Builder builder = new RemoteAddeEntry.Builder(name, dataset)
537                                                  .source(EntrySource.MCTABLE);
538            if (!defaultAcct) {
539                builder.account(username, project);
540            }
541            RemoteAddeEntry remoteEntry = builder.build();
542            logger.trace("built entry={}", remoteEntry);
543            entries.add(builder.build());
544        }
545        return entries;
546    }
547
548    private static Map<String, String> mapIpToName(
549        final Map<String, Set<String>> map) 
550    {
551        assert map != null;
552
553        Map<String, String> ipToName = newMap(map.size());
554        for (Entry<String, Set<String>> entry : map.entrySet()) {
555            Set<String> names = entry.getValue();
556            String displayName = "";
557            for (String name : names)
558                if (name.length() >= displayName.length())
559                    displayName = name;
560
561            if (displayName.isEmpty()) {
562                displayName = entry.getKey();
563            }
564            ipToName.put(entry.getKey(), displayName);
565        }
566        return ipToName;
567    }
568
569    private static Map<String, String> mapDatasetsToIp(final Map<String, String> datasets, final Map<String, String> hostMap) {
570        assert datasets != null;
571        assert hostMap != null;
572
573        Map<String, String> datasetToIp = newMap(datasets.size());
574        for (Entry<String, String> entry : datasets.entrySet()) {
575            String dataset = entry.getKey();
576            String alias = entry.getValue();
577            if (hostMap.containsKey(alias)) {
578                datasetToIp.put(dataset, hostMap.get(alias));
579            }
580        }
581        return datasetToIp;
582    }
583
584    /**
585     * Reads a {@literal "RESOLV.SRV"} file and converts the contents into a 
586     * {@link Set} of {@link LocalAddeEntry}s.
587     * 
588     * @param filename Filename containing desired {@code LocalAddeEntry}s. 
589     * Cannot be {@code null}.
590     * 
591     * @return {@code Set} of {@code LocalAddeEntry}s contained within 
592     * {@code filename}.
593     * 
594     * @throws IOException if there was a problem reading from {@code filename}.
595     * 
596     * @see #readResolvLine(String)
597     */
598    public static Set<LocalAddeEntry> readResolvFile(final String filename) throws IOException {
599        Set<LocalAddeEntry> servers = newLinkedHashSet();
600        BufferedReader br = null;
601        try {
602            br = new BufferedReader(new FileReader(filename));
603            String line;
604            while ((line = br.readLine()) != null) {
605                line = line.trim();
606                if (line.isEmpty()) {
607                    continue;
608                }
609                servers.add(readResolvLine(line));
610            }
611        } finally {
612            if (br != null) {
613                br.close();
614            }
615        }
616        return servers;
617    }
618
619    /**
620     * Converts a {@code String} containing a {@literal "RESOLV.SRV"} entry into
621     * a {@link LocalAddeEntry}.
622     */
623    public static LocalAddeEntry readResolvLine(String line) {
624        boolean disabled = line.startsWith("#");
625        if (disabled) {
626            line = line.substring(1);
627        }
628        Pattern commaSplit = Pattern.compile(",");
629        Pattern equalSplit = Pattern.compile("=");
630
631        String[] pairs = commaSplit.split(line.trim());
632        String[] pair;
633        Map<String, String> keyVals = new HashMap<String, String>(pairs.length);
634        for (int i = 0; i < pairs.length; i++) {
635            if (pairs[i] == null || pairs[i].isEmpty()) {
636                continue;
637            }
638
639            pair = equalSplit.split(pairs[i]);
640            if (pair.length != 2 || pair[0].isEmpty() || pair[1].isEmpty()) {
641                continue;
642            }
643
644            // group
645//            if ("N1".equals(pair[0])) {
646////                builder.group(pair[1]);
647//            }
648//            // descriptor/dataset
649//            else if ("N2".equals(pair[0])) {
650////                builder.descriptor(pair[1]);
651//            }
652//            // data type (only image supported?)
653//            else if ("TYPE".equals(pair[0])) {
654////                builder.type(strToEntryType(pair[1]));
655//            }
656//            // file format
657//            else if ("K".equals(pair[0])) {
658////                builder.kind(pair[1].toUpperCase());
659//            }
660//            // comment
661//            else if ("C".equals(pair[0])) {
662////                builder.name(pair[1]);
663//            }
664//            // mcv-specific; allows us to infer kind+type?
665//            else if ("MCV".equals(pair[0])) {
666////                builder.format(strToAddeFormat(pair[1]));
667//            }
668//            // realtime ("Y"/"N"/"A")
669//            else if ("RT".equals(pair[0])) {
670////                builder.realtime(pair[1]);
671//            }
672//            // start of file number range
673//            else if ("R1".equals(pair[0])) {
674////                builder.start(pair[1]);
675//            }
676//            // end of file number range
677//            else if ("R2".equals(pair[0])) {
678////                builder.end(pair[1]);
679//            }
680//            // filename mask
681            if ("MASK".equals(pair[0])) {
682                pair[1] = demungeFileMask(pair[1]);
683            }
684            keyVals.put(pair[0], pair[1]);
685        }
686
687        if (keyVals.containsKey("C") && keyVals.containsKey("N1") && keyVals.containsKey("MCV") && keyVals.containsKey("MASK")) {
688            LocalAddeEntry entry = new LocalAddeEntry.Builder(keyVals).build();
689            EntryStatus status = (disabled) ? EntryStatus.DISABLED : EntryStatus.ENABLED;
690            entry.setEntryStatus(status);
691            return entry;
692        } else {
693            return LocalAddeEntry.INVALID_ENTRY;
694        }
695    }
696
697    /**
698     * Writes a {@link Collection} of {@link LocalAddeEntry}s to a {@literal "RESOLV.SRV"}
699     * file. <b>This method discards the current contents of {@code filename}!</b>
700     * 
701     * @param filename Filename that will contain the {@code LocalAddeEntry}s within 
702     * {@code entries}. Cannot be {@code null}.
703     * 
704     * @param entries {@code Set} of entries to be written to {@code filename}.
705     * Cannot be {@code null}.
706     * 
707     * @throws IOException if there was a problem writing to {@code filename}.
708     * 
709     * @see #appendResolvFile(String, Collection)
710     */
711    public static void writeResolvFile(final String filename, final Collection<LocalAddeEntry> entries) throws IOException {
712        writeResolvFile(filename, false, entries);
713    }
714
715    /**
716     * Writes a {@link Collection} of {@link LocalAddeEntry}s to a {@literal "RESOLV.SRV"}
717     * file. This method will <i>append</i> the contents of {@code entries} to
718     * {@code filename}.
719     * 
720     * @param filename Filename that will contain the {@code LocalAddeEntry}s within 
721     * {@code entries}. Cannot be {@code null}.
722     * 
723     * @param entries {@code Collection} of entries to be written to {@code filename}.
724     * Cannot be {@code null}.
725     * 
726     * @throws IOException if there was a problem writing to {@code filename}.
727     * 
728     * @see #writeResolvFile(String, Collection)
729     */
730    public static void appendResolvFile(final String filename, final Collection<LocalAddeEntry> entries) throws IOException {
731        writeResolvFile(filename, true, entries);
732    }
733
734    /**
735     * Writes a {@link Collection} of {@link LocalAddeEntry}s to a {@literal "RESOLV.SRV"}
736     * file.
737     * 
738     * @param filename Filename that will contain the {@code LocalAddeEntry}s within 
739     * {@code entries}. Cannot be {@code null}.
740     * 
741     * @param append If {@code true}, append {@code entries} to {@code filename}. Otherwise discards contents of {@code filename}.
742     * 
743     * @param entries {@code Collection} of entries to be written to {@code filename}.
744     * Cannot be {@code null}. 
745     * 
746     * @throws IOException if there was a problem writing to {@code filename}.
747     * 
748     * @see #appendResolvFile(String, Collection)
749     * @see #asResolvEntry(LocalAddeEntry)
750     */
751    private static void writeResolvFile(final String filename, final boolean append, final Collection<LocalAddeEntry> entries) throws IOException {
752        BufferedWriter bw = null;
753        try {
754            bw = new BufferedWriter(new FileWriter(filename));
755            for (LocalAddeEntry entry : entries) {
756                bw.write(asResolvEntry(entry)+'\n');
757            }
758        } finally {
759            if (bw != null) {
760                bw.close();
761            }
762        }
763    }
764
765    /**
766     * De-munges file mask strings.
767     * 
768     * @throws NullPointerException if {@code path} is {@code null}. 
769     */
770    public static String demungeFileMask(final String path) {
771        Contract.notNull(path, "how dare you! null paths cannot be munged!");
772        int index = path.indexOf("/*");
773        if (index < 0) {
774            return path;
775        }
776        String tmpFileMask = path.substring(0, index);
777        /** Look for "cygwinPrefix" at start of string and munge accordingly */
778        if (tmpFileMask.length() > cygwinPrefixLength+1 &&
779            tmpFileMask.substring(0,cygwinPrefixLength).equals(cygwinPrefix)) {
780            String driveLetter = tmpFileMask.substring(cygwinPrefixLength,cygwinPrefixLength+1).toUpperCase();
781            return driveLetter + ':' + tmpFileMask.substring(cygwinPrefixLength+1).replace('/', '\\');
782        } else {
783            return tmpFileMask;
784        }
785    }
786
787    /**
788     * Munges a file mask {@link String} into something {@literal "RESOLV.SRV"}
789     * expects.
790     * 
791     * <p>Munging is only needed for Windows users--the process converts 
792     * back slashes into forward slashes and prefixes with {@literal "/cygdrive/"}.
793     * 
794     * @throws NullPointerException if {@code mask} is {@code null}.
795     */
796    public static String mungeFileMask(final String mask) {
797        Contract.notNull(mask, "Cannot further munge this mask; it was null upon arriving");
798        StringBuilder s = new StringBuilder(100);
799        if (mask.length() > 3 && ":".equals(mask.substring(1, 2))) {
800            String newFileMask = mask;
801            String driveLetter = newFileMask.substring(0,1).toLowerCase();
802            newFileMask = newFileMask.substring(3);
803            newFileMask = newFileMask.replace('\\', '/');
804            s.append("/cygdrive/").append(driveLetter).append('/').append(newFileMask);
805        } else {
806            s.append("").append(mask);
807        }
808        return s.toString();
809    }
810
811    /**
812     * Converts a {@link Collection} of {@link LocalAddeEntry}s into a {@link List}
813     * of {@code String}s. 
814     * 
815     * @param entries {@code Collection} of entries to convert. Should not be {@code null}.
816     * 
817     * @return {@code entries} represented as {@code String}s.
818     * 
819     * @see #asResolvEntry(LocalAddeEntry)
820     */
821    public static List<String> asResolvEntries(final Collection<LocalAddeEntry> entries) {
822        List<String> resolvEntries = arrList(entries.size());
823        for (LocalAddeEntry entry : entries) {
824            resolvEntries.add(asResolvEntry(entry));
825        }
826        return resolvEntries;
827    }
828
829    /**
830     * Converts a given {@link LocalAddeEntry} into a {@code String} that is 
831     * suitable for including in a {@literal "RESOLV.SRV"} file. This method
832     * does <b>not</b> append a newline to the end of the {@code String}.
833     * 
834     * @param entry The {@code LocalAddeEntry} to convert. Should not be {@code null}.
835     * 
836     * @return {@code entry} as a {@literal "RESOLV.SRV"} entry.
837     */
838    public static String asResolvEntry(final LocalAddeEntry entry) {
839        AddeFormat format = entry.getFormat();
840        ServerName servName = format.getServerName();
841
842        StringBuilder s = new StringBuilder(100);
843        if (entry.getEntryStatus() != EntryStatus.ENABLED) {
844            s.append('#');
845        }
846        s.append("N1=").append(entry.getGroup().toUpperCase())
847            .append(",N2=").append(entry.getDescriptor().toUpperCase())
848            .append(",TYPE=").append(format.getType())
849            .append(",RT=").append(entry.getRealtimeAsString())
850            .append(",K=").append(format.getServerName())
851            .append(",R1=").append(entry.getStart())
852            .append(",R2=").append(entry.getEnd())
853            .append(",MCV=").append(format.name())
854            .append(",C=").append(entry.getName());
855
856        if (servName == ServerName.LV1B) {
857            s.append(",Q=LALO");
858        }
859
860        String tmpFileMask = entry.getFileMask();
861        if (tmpFileMask.length() > 3 && ":".equals(tmpFileMask.substring(1, 2))) {
862            String newFileMask = tmpFileMask;
863            String driveLetter = newFileMask.substring(0,1).toLowerCase();
864            newFileMask = newFileMask.substring(3);
865            newFileMask = newFileMask.replace('\\', '/');
866            s.append(",MASK=/cygdrive/").append(driveLetter).append('/').append(newFileMask);
867        } else {
868            s.append(",MASK=").append(tmpFileMask);
869        }
870        // local servers seem to really like trailing commas!
871        return s.append('/').append(format.getFileFilter()).append(',').toString(); 
872    }
873}