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