001 /* 002 * $Id: SystemState.java,v 1.12 2012/02/19 17:35:52 davep 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 package edu.wisc.ssec.mcidasv.util; 031 032 import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.arrList; 033 import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.cast; 034 import static edu.wisc.ssec.mcidasv.util.CollectionHelpers.newLinkedHashMap; 035 036 import java.io.InputStream; 037 import java.lang.management.ManagementFactory; 038 import java.lang.management.OperatingSystemMXBean; 039 import java.lang.reflect.Method; 040 041 import java.util.List; 042 import java.util.Map; 043 import java.util.Properties; 044 import java.util.TreeSet; 045 046 import java.awt.DisplayMode; 047 import java.awt.GraphicsConfiguration; 048 import java.awt.GraphicsDevice; 049 import java.awt.GraphicsEnvironment; 050 import java.awt.Rectangle; 051 052 import javax.media.j3d.Canvas3D; 053 import javax.media.j3d.VirtualUniverse; 054 055 import org.python.core.Py; 056 import org.python.core.PySystemState; 057 058 import org.slf4j.Logger; 059 import org.slf4j.LoggerFactory; 060 061 import ucar.unidata.idv.ArgsManager; 062 import ucar.unidata.idv.IdvResourceManager.IdvResource; 063 import ucar.unidata.util.ResourceCollection; 064 import ucar.visad.display.DisplayUtil; 065 066 import edu.wisc.ssec.mcidasv.Constants; 067 import edu.wisc.ssec.mcidasv.McIDASV; 068 import edu.wisc.ssec.mcidasv.StateManager; 069 070 /** 071 * Utility methods for querying the state of the user's machine. 072 */ 073 public class SystemState { 074 075 /** Handy logging object. */ 076 private static final Logger logger = LoggerFactory.getLogger(SystemState.class); 077 078 // Don't allow outside instantiation. 079 private SystemState() { } 080 081 public static String escapeWhitespaceChars(final CharSequence sequence) { 082 StringBuilder sb = new StringBuilder(sequence.length() * 7); 083 for (int i = 0; i < sequence.length(); i++) { 084 switch (sequence.charAt(i)) { 085 case '\t': sb.append("\\t"); break; 086 case '\n': sb.append('\\').append('n'); break; 087 case '\013': sb.append("\\013"); break; 088 case '\f': sb.append("\\f"); break; 089 case '\r': sb.append("\\r"); break; 090 case '\u0085': sb.append("\\u0085"); break; 091 case '\u1680': sb.append("\\u1680"); break; 092 case '\u2028': sb.append("\\u2028"); break; 093 case '\u2029': sb.append("\\u2029"); break; 094 case '\u205f': sb.append("\\u205f"); break; 095 case '\u3000': sb.append("\\u3000"); break; 096 } 097 } 098 logger.trace("incoming={} outgoing={}", sequence.length(), sb.length()); 099 return sb.toString(); 100 } 101 102 /** 103 * Attempt to invoke {@code OperatingSystemMXBean.methodName} via 104 * reflection. 105 * 106 * @param <T> Either {@code Long} or {@code Double}. 107 * @param methodName The method to invoke. Must belong to 108 * {@code com.sun.management.OperatingSystemMXBean}. 109 * @param defaultValue Default value to return, must be in 110 * {@literal "boxed"} form. 111 * 112 * @return Either the result of the {@code methodName} call or 113 * {@code defaultValue}. 114 */ 115 private static <T> T hackyMethodCall(final String methodName, final T defaultValue) { 116 assert methodName != null : "Cannot invoke a null method name"; 117 assert methodName.length() > 0: "Cannot invoke an empty method name"; 118 OperatingSystemMXBean osBean = 119 ManagementFactory.getOperatingSystemMXBean(); 120 T result = defaultValue; 121 try { 122 Method m = osBean.getClass().getMethod(methodName); 123 m.setAccessible(true); 124 // don't suppress warnings because we cannot guarantee that this 125 // cast is correct. 126 result = (T)m.invoke(osBean); 127 } catch (Exception e) { 128 logger.error("couldn't call method: " + methodName, e); 129 } 130 return result; 131 } 132 133 /** 134 * Returns the contents of Jython's registry (basically just Jython-specific 135 * properties) as well as some of the information from Python's 136 * {@literal "sys"} module. 137 * 138 * @return Jython's configuration settings. 139 */ 140 public static Map<Object, Object> queryJythonProps() { 141 Map<Object, Object> properties = newLinkedHashMap(PySystemState.registry); 142 properties.put("sys.argv", Py.getSystemState().argv.toString()); 143 properties.put("sys.builtin_module_names", PySystemState.builtin_module_names.toString()); 144 properties.put("sys.byteorder", PySystemState.byteorder); 145 properties.put("sys.isPackageCacheEnabled", PySystemState.isPackageCacheEnabled()); 146 properties.put("sys.path", Py.getSystemState().path); 147 properties.put("sys.platform", PySystemState.platform.toString()); 148 properties.put("sys.version", PySystemState.version); 149 properties.put("sys.version_info", PySystemState.version_info); 150 return properties; 151 } 152 153 /** 154 * Attempts to call methods belonging to 155 * {@code com.sun.management.OperatingSystemMXBean}. If successful, we'll 156 * have the following information: 157 * <ul> 158 * <li>opsys.memory.virtual.committed: virtual memory that is guaranteed to be available</li> 159 * <li>opsys.memory.swap.total: total amount of swap space in bytes</li> 160 * <li>opsys.memory.swap.free: free swap space in bytes</li> 161 * <li>opsys.cpu.time: CPU time used by the process (nanoseconds)</li> 162 * <li>opsys.memory.physical.free: free physical memory in bytes</li> 163 * <li>opsys.memory.physical.total: physical memory in bytes</li> 164 * <li>opsys.load: system load average for the last minute</li> 165 * </ul> 166 * 167 * @return Map of properties that contains interesting information about 168 * the hardware McIDAS-V is using. 169 */ 170 public static Map<String, String> queryOpSysProps() { 171 Map<String, String> properties = newLinkedHashMap(10); 172 long committed = hackyMethodCall("getCommittedVirtualMemorySize", Long.MIN_VALUE); 173 long freeMemory = hackyMethodCall("getFreePhysicalMemorySize", Long.MIN_VALUE); 174 long freeSwap = hackyMethodCall("getFreeSwapSpaceSize", Long.MIN_VALUE); 175 long cpuTime = hackyMethodCall("getProcessCpuTime", Long.MIN_VALUE); 176 long totalMemory = hackyMethodCall("getTotalPhysicalMemorySize", Long.MIN_VALUE); 177 long totalSwap = hackyMethodCall("getTotalSwapSpaceSize", Long.MIN_VALUE); 178 double loadAvg = hackyMethodCall("getSystemLoadAverage", Double.NaN); 179 180 Runtime rt = Runtime.getRuntime(); 181 long currentMem = rt.totalMemory() - rt.freeMemory(); 182 183 properties.put("opsys.cpu.time", Long.toString(cpuTime)); 184 properties.put("opsys.load", Double.toString(loadAvg)); 185 properties.put("opsys.memory.jvm.current", Long.toString(currentMem)); 186 properties.put("opsys.memory.jvm.max", Long.toString(rt.maxMemory())); 187 properties.put("opsys.memory.virtual.committed", Long.toString(committed)); 188 properties.put("opsys.memory.physical.free", Long.toString(freeMemory)); 189 properties.put("opsys.memory.physical.total", Long.toString(totalMemory)); 190 properties.put("opsys.memory.swap.free", Long.toString(freeSwap)); 191 properties.put("opsys.memory.swap.total", Long.toString(totalSwap)); 192 193 return properties; 194 } 195 196 /** 197 * Polls Java for information about the user's machine. We're specifically 198 * after memory statistics, number of processors, and display information. 199 * 200 * @return {@link Map} of properties that describes the user's machine. 201 */ 202 public static Map<String, String> queryMachine() { 203 Map<String, String> props = newLinkedHashMap(); 204 205 // cpu count and whatnot 206 int processors = Runtime.getRuntime().availableProcessors(); 207 props.put("opsys.cpu.count", Integer.toString(processors)); 208 209 // memory: available, used, etc 210 props.putAll(queryOpSysProps()); 211 212 // screen: count, resolution(s) 213 GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); 214 int displayCount = ge.getScreenDevices().length; 215 216 for (int i = 0; i < displayCount; i++) { 217 String baseId = "opsys.display."+i+'.'; 218 GraphicsDevice dev = ge.getScreenDevices()[i]; 219 DisplayMode mode = dev.getDisplayMode(); 220 props.put(baseId+"name", dev.getIDstring()); 221 props.put(baseId+"depth", Integer.toString(mode.getBitDepth())); 222 props.put(baseId+"width", Integer.toString(mode.getWidth())); 223 props.put(baseId+"height", Integer.toString(mode.getHeight())); 224 props.put(baseId+"refresh", Integer.toString(mode.getRefreshRate())); 225 } 226 return props; 227 } 228 229 /** 230 * Returns a mapping of display number to a {@link java.awt.Rectangle} 231 * that represents the {@literal "bounds"} of the display. 232 * 233 * @return Rectangles representing the {@literal "bounds"} of the current 234 * display devices. 235 */ 236 public static Map<Integer, Rectangle> getDisplayBounds() { 237 GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); 238 int idx = 0; 239 Map<Integer, Rectangle> map = newLinkedHashMap(ge.getScreenDevices().length * 2); 240 for (GraphicsDevice dev : ge.getScreenDevices()) { 241 for (GraphicsConfiguration config : dev.getConfigurations()) { 242 map.put(idx++, config.getBounds()); 243 } 244 } 245 return map; 246 } 247 248 // TODO(jon): this should really be a polygon 249 public static Rectangle getVirtualDisplayBounds() { 250 Rectangle virtualBounds = new Rectangle(); 251 for (Rectangle bounds : getDisplayBounds().values()) { 252 virtualBounds = virtualBounds.union(bounds); 253 } 254 return virtualBounds; 255 } 256 257 /** 258 * Polls Java 3D for information about its environment. Specifically, we 259 * call {@link VirtualUniverse#getProperties()} and 260 * {@link Canvas3D#queryProperties()}. 261 * 262 * @return As much information as Java 3D can provide. 263 */ 264 @SuppressWarnings("unchecked") // casting to Object, so this should be fine. 265 public static Map<String, Object> queryJava3d() { 266 267 Map<String, Object> universeProps = 268 (Map<String, Object>)VirtualUniverse.getProperties(); 269 270 GraphicsConfiguration config = 271 DisplayUtil.getPreferredConfig(null, true, false); 272 Map<String, Object> c3dMap = new Canvas3D(config).queryProperties(); 273 274 Map<String, Object> props = 275 newLinkedHashMap(universeProps.size() + c3dMap.size()); 276 props.putAll(universeProps); 277 props.putAll(c3dMap); 278 return props; 279 } 280 281 /** 282 * Gets a human-friendly representation of the information embedded within 283 * IDV's {@code build.properties}. 284 * 285 * @return {@code String} that looks like {@literal "IDV version major.minor<b>revision</b> built <b>date</b>"}. 286 * For example: {@code IDV version 2.9u4 built 2011-04-13 14:01 UTC}. 287 */ 288 public static String getIdvVersionString() { 289 Map<String, String> info = queryIdvBuildProperties(); 290 return "IDV version " + info.get("idv.version.major") + '.' + 291 info.get("idv.version.minor") + info.get("idv.version.revision") + 292 " built " + info.get("idv.build.date"); 293 } 294 295 /** 296 * Gets a human-friendly representation of the information embedded within 297 * McIDAS-V's {@code build.properties}. 298 * 299 * @return {@code String} that looks like {@literal "McIDAS-V version major.minor<b>release</b> built <b>date</b>"}. 300 * For example: {@code McIDAS-V version 1.02beta1 built 2011-04-14 17:36}. 301 */ 302 public static String getMcvVersionString() { 303 Map<String, String> info = queryMcvBuildProperties(); 304 return "McIDAS-V version " + info.get(Constants.PROP_VERSION_MAJOR) + '.' + 305 info.get(Constants.PROP_VERSION_MINOR) + info.get(Constants.PROP_VERSION_RELEASE) + 306 " built " + info.get(Constants.PROP_BUILD_DATE); 307 } 308 309 private InputStream getResourceAsStream(final String name) { 310 return ClassLoader.getSystemResourceAsStream(name); 311 } 312 313 /** 314 * Returns a {@link Map} of the (currently) most useful contents of 315 * {@code ucar/unidata/idv/resources/build.properties}. 316 * 317 * <p>Consider the output of {@link #getIdvVersionString()}; it's built 318 * with the the following: 319 * <ul> 320 * <li><b>{@code idv.version.major}</b>: currently {@literal "2"}</li> 321 * <li><b>{@code idv.version.minor}</b>: currently {@literal "9"}</li> 322 * <li><b>{@code idv.version.revision}</b>: currently {@literal "u4"}}</li> 323 * <li><b>{@code idv.build.date}</b>: varies pretty frequently, 324 * as it's the build timestamp for idv.jar</li> 325 * </ul> 326 * 327 * @return A {@code Map} of at least the useful parts of build.properties. 328 */ 329 public static Map<String, String> queryIdvBuildProperties() { 330 SystemState sysState = new SystemState(); 331 Map<String, String> versions = newLinkedHashMap(4); 332 InputStream input = null; 333 try { 334 input = sysState.getResourceAsStream("ucar/unidata/idv/resources/build.properties"); 335 Properties props = new Properties(); 336 props.load(input); 337 String major = props.getProperty("idv.version.major", "no_major"); 338 String minor = props.getProperty("idv.version.minor", "no_minor"); 339 String revision = props.getProperty("idv.version.revision", "no_revision"); 340 String date = props.getProperty("idv.build.date", ""); 341 versions.put("idv.version.major", major); 342 versions.put("idv.version.minor", minor); 343 versions.put("idv.version.revision", revision); 344 versions.put("idv.build.date", date); 345 } catch (Exception e) { 346 logger.error("could not read from IDV build.properties", e); 347 } finally { 348 sysState = null; 349 if (input != null) { 350 try { 351 input.close(); 352 } catch (Exception ex) { 353 logger.error("could not close IDV build.properties", ex); 354 } 355 } 356 } 357 return versions; 358 } 359 360 /** 361 * Returns a {@link Map} of the (currently) most useful contents of 362 * {@code edu/wisc/ssec/mcidasv/resources/build.properties}. 363 * 364 * <p>Consider the output of {@link #getMcvVersionString()}; it's built 365 * with the the following: 366 * <ul> 367 * <li><b>{@code mcidasv.version.major}</b>: 368 * currently {@literal "1"}</li> 369 * <li><b>{@code mcidasv.version.minor}</b>: 370 * currently {@literal "02"}</li> 371 * <li><b>{@code mcidasv.version.release}</b>: currently 372 * {@literal "beta1"}</li> 373 * <li><b>{@code mcidasv.build.date}</b>: varies pretty frequently, as 374 * it's the build timestamp for mcidasv.jar.</li> 375 * </ul> 376 * 377 * @return A {@code Map} of at least the useful parts of build.properties. 378 */ 379 public static Map<String, String> queryMcvBuildProperties() { 380 SystemState sysState = new SystemState(); 381 Map<String, String> versions = newLinkedHashMap(4); 382 InputStream input = null; 383 try { 384 input = sysState.getResourceAsStream("edu/wisc/ssec/mcidasv/resources/build.properties"); 385 Properties props = new Properties(); 386 props.load(input); 387 String major = props.getProperty(Constants.PROP_VERSION_MAJOR, "0"); 388 String minor = props.getProperty(Constants.PROP_VERSION_MINOR, "0"); 389 String release = props.getProperty(Constants.PROP_VERSION_RELEASE, ""); 390 String date = props.getProperty(Constants.PROP_BUILD_DATE, "Unknown"); 391 versions.put(Constants.PROP_VERSION_MAJOR, major); 392 versions.put(Constants.PROP_VERSION_MINOR, minor); 393 versions.put(Constants.PROP_VERSION_RELEASE, release); 394 versions.put(Constants.PROP_BUILD_DATE, date); 395 } catch (Exception e) { 396 logger.error("could not read from McIDAS-V build.properties!", e); 397 } finally { 398 sysState = null; 399 if (input != null) { 400 try { 401 input.close(); 402 } catch (Exception ex) { 403 logger.error("could not close McIDAS-V build.properties!", ex); 404 } 405 } 406 } 407 return versions; 408 } 409 410 /** 411 * Queries McIDAS-V for information about its state. There's not a good way 412 * to characterize what we're interested in, so let's leave it at 413 * {@literal "whatever seems useful"}. 414 * 415 * @return Information about the state of McIDAS-V. 416 */ 417 // need: argsmanager, resource manager 418 public static Map<String, Object> queryMcvState(final McIDASV mcv) { 419 Map<String, Object> props = newLinkedHashMap(); 420 421 ArgsManager args = mcv.getArgsManager(); 422 props.put("mcv.state.islinteractive", args.getIslInteractive()); 423 props.put("mcv.state.offscreen", args.getIsOffScreen()); 424 props.put("mcv.state.initcatalogs", args.getInitCatalogs()); 425 props.put("mcv.state.actions", mcv.getActionHistory()); 426 props.put("mcv.plugins.installed", args.installPlugins); 427 props.put("mcv.state.commandline", mcv.getCommandLineArgs()); 428 429 // loop through resources 430 List<IdvResource> resources = 431 cast(mcv.getResourceManager().getResources()); 432 for (IdvResource resource : resources) { 433 String id = resource.getId(); 434 props.put(id+".description", resource.getDescription()); 435 if (resource.getPattern() == null) { 436 props.put(id+".pattern", "null"); 437 } else { 438 props.put(id+".pattern", resource.getPattern()); 439 } 440 441 ResourceCollection rc = mcv.getResourceManager().getResources(resource); 442 int rcSize = rc.size(); 443 List<String> specified = arrList(rcSize); 444 List<String> valid = arrList(rcSize); 445 for (int i = 0; i < rcSize; i++) { 446 String tmpResource = (String)rc.get(i); 447 specified.add(tmpResource); 448 if (rc.isValid(i)) { 449 valid.add(tmpResource); 450 } 451 } 452 453 props.put(id+".specified", specified); 454 props.put(id+".existing", valid); 455 } 456 return props; 457 } 458 459 /** 460 * Builds a (filtered) subset of the McIDAS-V system properties and returns 461 * the results as a {@code String}. 462 * 463 * @return The McIDAS-V system properties in the following format: 464 * {@code KEY=VALUE\n}. This is so we kinda-sorta conform to the standard 465 * {@link Properties} file format. 466 * 467 * @see #getStateAsString(edu.wisc.ssec.mcidasv.McIDASV, boolean) 468 */ 469 public static String getStateAsString(final McIDASV mcv) { 470 return getStateAsString(mcv, false); 471 } 472 473 /** 474 * Builds the McIDAS-V system properties and returns the results as a 475 * {@code String}. 476 * 477 * @param firehose If {@code true}, enables {@literal "unfiltered"} output. 478 * 479 * @return The McIDAS-V system properties in the following format: 480 * {@code KEY=VALUE\n}. This is so we kinda-sorta conform to the standard 481 * {@link Properties} file format. 482 */ 483 public static String getStateAsString(final McIDASV mcv, final boolean firehose) { 484 int builderSize = (firehose) ? 45000 : 1000; 485 StringBuilder buf = new StringBuilder(builderSize); 486 487 Map<String, String> versions = ((StateManager)mcv.getStateManager()).getVersionInfo(); 488 Properties sysProps = System.getProperties(); 489 Map<String, Object> j3dProps = queryJava3d(); 490 Map<String, String> machineProps = queryMachine(); 491 Map<Object, Object> jythonProps = queryJythonProps(); 492 Map<String, Object> mcvProps = queryMcvState(mcv); 493 494 if (sysProps.contains("line.separator")) { 495 sysProps.put("line.separator", escapeWhitespaceChars((String)sysProps.get("line.separator"))); 496 logger.trace("grr='{}'", sysProps.get("line.separator")); 497 } 498 499 String maxMem = Long.toString(Long.valueOf(machineProps.get("opsys.memory.jvm.max")) / 1048576L); 500 String curMem = Long.toString(Long.valueOf(machineProps.get("opsys.memory.jvm.current")) / 1048576L); 501 502 buf.append("# Software Versions:") 503 .append("\n# McIDAS-V: ").append(versions.get("mcv.version.general")).append(" (").append(versions.get("mcv.version.build")).append(')') 504 .append("\n# IDV: ").append(versions.get("idv.version.general")).append(" (").append(versions.get("idv.version.build")).append(')') 505 .append("\n\n# Operating System:") 506 .append("\n# Name: ").append(sysProps.getProperty("os.name")) 507 .append("\n# Version: ").append(sysProps.getProperty("os.version")) 508 .append("\n# Architecture: ").append(sysProps.getProperty("os.arch")) 509 .append("\n\n# Java:") 510 .append("\n# Version: ").append(sysProps.getProperty("java.version")) 511 .append("\n# Vendor: ").append(sysProps.getProperty("java.vendor")) 512 .append("\n# Home: ").append(sysProps.getProperty("java.home")) 513 .append("\n\n# JVM Memory") 514 .append("\n# Current: ").append(curMem).append(" MB") 515 .append("\n# Maximum: ").append(maxMem).append(" MB") 516 .append("\n\n# Java 3D:") 517 .append("\n# Renderer: ").append(j3dProps.get("j3d.renderer")) 518 .append("\n# Pipeline: ").append(j3dProps.get("j3d.pipeline")) 519 .append("\n# Vendor: ").append(j3dProps.get("j3d.vendor")) 520 .append("\n# Version: ").append(j3dProps.get("j3d.version")) 521 .append("\n\n# Jython:") 522 .append("\n# Version: ").append(jythonProps.get("sys.version_info")) 523 .append("\n# python.home: ").append(jythonProps.get("python.home")); 524 525 if (firehose) { 526 buf.append("\n\n\n#Firehose:\n\n# SOFTWARE VERSIONS\n"); 527 for (String key : (new TreeSet<String>(versions.keySet()))) { 528 buf.append(key).append('=').append(versions.get(key)).append('\n'); 529 } 530 531 buf.append("\n# MACHINE PROPERTIES\n"); 532 for (String key : (new TreeSet<String>(machineProps.keySet()))) { 533 buf.append(key).append('=').append(machineProps.get(key)).append('\n'); 534 } 535 536 buf.append("\n# JAVA SYSTEM PROPERTIES\n"); 537 for (Object key : (new TreeSet<Object>(sysProps.keySet()))) { 538 buf.append(key).append('=').append(sysProps.get(key)).append('\n'); 539 } 540 541 buf.append("\n# JAVA3D/JOGL PROPERTIES\n"); 542 for (String key : (new TreeSet<String>(j3dProps.keySet()))) { 543 buf.append(key).append('=').append(j3dProps.get(key)).append('\n'); 544 } 545 546 buf.append("\n# JYTHON PROPERTIES\n"); 547 for (Object key : (new TreeSet<Object>(jythonProps.keySet()))) { 548 buf.append(key).append('=').append(jythonProps.get(key)).append('\n'); 549 } 550 551 // get idv/mcv properties 552 buf.append("\n# IDV AND MCIDAS-V PROPERTIES\n"); 553 for (String key : (new TreeSet<String>(mcvProps.keySet()))) { 554 buf.append(key).append('=').append(mcvProps.get(key)).append('\n'); 555 } 556 } 557 return buf.toString(); 558 } 559 }