001/* 002 * This file is part of McIDAS-V 003 * 004 * Copyright 2007-2016 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.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:"+EntryStore.getLocalPort(), "<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 s.append(",MASK=/cygdrive/").append(driveLetter).append('/').append(newFileMask); 1037 } else { 1038 s.append(",MASK=").append(tmpFileMask); 1039 } 1040 // local servers seem to really like trailing commas! 1041 return s.append('/').append(format.getFileFilter()).append(',').toString(); 1042 } 1043}