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