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