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 }