PageRenderTime 25ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/plugins/net.bioclipse.r.business/src/net/bioclipse/r/business/RBusinessManager.java

https://github.com/bioclipse/bioclipse.statistics
Java | 677 lines | 583 code | 39 blank | 55 comment | 64 complexity | 532edda281d0db2d55366ed9d53318da MD5 | raw file
  1. /*******************************************************************************
  2. * Copyright (c) 2016 Egon Willighagen <egon.willighagen@gmail.com>
  3. * Christian Ander <christian.ander@gmail.com>
  4. * Valentin Georgiev <valentin.georgiev@farmbio.uu.se>
  5. *
  6. * All rights reserved. This program and the accompanying materials
  7. * are made available under the terms of the Eclipse Public License v1.0
  8. * which accompanies this distribution, and is available at
  9. * http://www.eclipse.org/legal/epl-v10.html
  10. *
  11. * Contact: http://www.bioclipse.net/
  12. ******************************************************************************/
  13. package net.bioclipse.r.business;
  14. import java.io.BufferedReader;
  15. import java.io.File;
  16. import java.io.FileNotFoundException;
  17. import java.io.FilenameFilter;
  18. import java.io.IOException;
  19. import java.io.InputStream;
  20. import java.io.InputStreamReader;
  21. import java.io.StringWriter;
  22. import java.net.MalformedURLException;
  23. import java.net.URL;
  24. import java.util.NoSuchElementException;
  25. import java.util.regex.Pattern;
  26. import javax.security.auth.login.LoginException;
  27. import net.bioclipse.business.BioclipsePlatformManager;
  28. import net.bioclipse.core.business.BioclipseException;
  29. import net.bioclipse.core.util.FileUtil;
  30. import net.bioclipse.managers.business.IBioclipseManager;
  31. import net.bioclipse.r.RServiManager;
  32. import net.bioclipse.statistics.model.IMatrixResource;
  33. import org.apache.log4j.Logger;
  34. import org.eclipse.core.resources.IWorkspace;
  35. import org.eclipse.core.resources.IWorkspaceRoot;
  36. import org.eclipse.core.resources.ResourcesPlugin;
  37. import org.eclipse.core.runtime.CoreException;
  38. import org.eclipse.core.runtime.IPath;
  39. import org.eclipse.swt.widgets.Display;
  40. import de.walware.rj.data.RObject;
  41. import de.walware.rj.data.RStore;
  42. import de.walware.rj.servi.RServi;
  43. public class RBusinessManager implements IBioclipseManager {
  44. private static final Logger logger = Logger.getLogger(RBusinessManager.class);
  45. private RServi rservi;
  46. public String R_HOME;
  47. public String r_path;
  48. private String status = "";
  49. private Boolean working = true;
  50. private IPath workspacePath;
  51. private static final String OS = System.getProperty("os.name").toString();
  52. private RServiManager rsmanager;
  53. private boolean rightRVersion = true;
  54. public static String NEWLINE = System.getProperty("line.separator");
  55. public static String cmdparser = "(;?\r?\n|;)";
  56. public static final String fileseparator = java.io.File.separator;
  57. public static final String R_CONSOLE_ERR_MESSAGE = "See http://pele.farmbio.uu.se/bioclipse/help/nav/14_1 or the internal Bioclipse help system!";
  58. public RBusinessManager() throws LoginException, NoSuchElementException {
  59. logger.info("Starting R manager");
  60. IWorkspace workspace = ResourcesPlugin.getWorkspace();
  61. IWorkspaceRoot root = workspace.getRoot();
  62. workspacePath = root.getLocation();
  63. logger.debug("Bioclipse working directory: " + workspacePath.toString());
  64. //Read R_HOME from prefs
  65. R_HOME
  66. = Activator.getDefault().getPreferenceStore()
  67. .getString( net.bioclipse.r.business
  68. .Activator.PREF_R_HOME );
  69. logger.debug("Pref R_home: "+ R_HOME);
  70. boolean emptyRhome=false;
  71. if (R_HOME.isEmpty()) {
  72. emptyRhome=true;
  73. R_HOME = System.getenv("R_HOME");
  74. }
  75. logger.debug("R_HOME=" + R_HOME);
  76. try {
  77. r_path = checkRPath();
  78. R_HOME = checkR_HOME(R_HOME); // check if R_HOME is correct
  79. checkRdependencies(); // check if we run right R version and all plug-ins are installed in R
  80. //If the preference was empty, we have discovered a new RHOME
  81. if (emptyRhome){
  82. //Set default preference
  83. Activator.getDefault().getPreferenceStore()
  84. .setDefault(net.bioclipse.r.business
  85. .Activator.PREF_R_HOME, R_HOME);
  86. //Set current preference
  87. Activator.getDefault().getPreferenceStore()
  88. .setValue(net.bioclipse.r.business
  89. .Activator.PREF_R_HOME, R_HOME);
  90. }
  91. }
  92. catch (FileNotFoundException e) {
  93. working = false;
  94. status = e.getMessage();
  95. }
  96. catch (BioclipseException e) {
  97. working = false;
  98. status = e.getMessage();
  99. }
  100. rservi = getInitR("Rconsole");
  101. }
  102. public RServi getInitR(String task) {
  103. rsmanager = new RServiManager(task);
  104. // next, check if there are $HOME-based lib paths
  105. String userLibPath = checkUserLibDir();
  106. logger.debug("User path: " + userLibPath);
  107. try {
  108. rsmanager.setEmbedded(R_HOME, userLibPath); // Start Rservi
  109. } catch (CoreException e) {
  110. // TODO Auto-generated catch block
  111. working = false;
  112. logger.error( "Init R", e );
  113. status = e.getMessage();
  114. e.printStackTrace();
  115. }
  116. if (working) {
  117. try {
  118. rservi = rsmanager.getRServi(task);
  119. initSession();
  120. return rservi;
  121. }
  122. catch (CoreException e) {
  123. working = false;
  124. logger.error( "Init R", e );
  125. status = e.getMessage();
  126. }
  127. }
  128. if (!working) {
  129. logger.error(status);
  130. status += NEWLINE + R_CONSOLE_ERR_MESSAGE;
  131. return null;
  132. }
  133. return rservi;
  134. }
  135. /**
  136. * The old eval method that can be called without RServi
  137. * @param command
  138. * @return result
  139. */
  140. public String eval(String command) {
  141. String result;
  142. result = eval(command, rservi);
  143. return result;
  144. }
  145. /**
  146. * Gives a short one word name of the manager used as variable name when
  147. * scripting.
  148. */
  149. public String getManagerName() {
  150. return "r";
  151. }
  152. public String getStatus() {
  153. return status;
  154. }
  155. public Boolean isWorking() {
  156. return working;
  157. }
  158. /**
  159. * Run system commands
  160. */
  161. public boolean runCmd(String command) {
  162. logger.debug(command);
  163. StringBuilder s = new StringBuilder();
  164. String line = null;
  165. boolean result = false; // if command is successful
  166. try {
  167. Runtime rt = Runtime.getRuntime();
  168. Process pr;
  169. if (OS.startsWith("Mac")) {
  170. pr = rt.exec(new String[] { "bash", "-c", r_path + command });
  171. }
  172. else if (OS.startsWith("Windows")) {
  173. String prog = R_HOME + "\\bin\\" + "R";
  174. pr = rt.exec( new String[] { prog, "-e", command, "-s" });
  175. }
  176. else if (OS.startsWith("Linux"))
  177. // TODO check if Linux command is working
  178. pr = rt.exec(new String[] { "sh", "-c", command });
  179. else
  180. pr = rt.exec(command);
  181. int exitVal = pr.waitFor();
  182. if (exitVal != 0) { // Command fail
  183. BufferedReader input = new BufferedReader(new InputStreamReader(pr.getErrorStream()));
  184. s.append("ERROR: ");
  185. while((line=input.readLine()) != null) {
  186. s.append(line);
  187. s.append("\n");
  188. }
  189. }
  190. else { // Command success
  191. BufferedReader input = new BufferedReader(new InputStreamReader(pr.getInputStream()));
  192. while((line=input.readLine()) != null) {
  193. s.append(line);
  194. s.append("\n");
  195. }
  196. result = true; // R CMD INSTALL --no-test-load package.tar.gz does not return output that can be gotten by getInputStream
  197. }
  198. } catch(Exception e) {
  199. working = false;
  200. status = e.getMessage();
  201. logger.error( status, e );
  202. }
  203. status=s.toString();
  204. logger.debug(status);
  205. return result;
  206. }
  207. private boolean runRCmd(String Rcommand) {
  208. if (OS.startsWith("Windows")) {
  209. Rcommand = Rcommand.replace("R -e \"", "");
  210. Rcommand = Rcommand.replace("\" -s", "");
  211. return runCmd(Rcommand);
  212. }
  213. else return runCmd(Rcommand);
  214. }
  215. /**
  216. * Check if R version is above 2.12.9
  217. * and if all R dependencies are installed, such as "rj", "rJava", and "bc2r"
  218. * @throws BioclipseException
  219. */
  220. private void checkRdependencies() throws FileNotFoundException, BioclipseException {
  221. runRCmd("R -e \"getRversion()\" -s");
  222. int st = compare(status.substring(5, (status.length() - 2)), "2.15");
  223. if (st < 0) {
  224. rightRVersion = false;
  225. throw new BioclipseException("Incompatible R version, Runnig R within Bioclipse requires R version 2.13, or later!");
  226. }
  227. logger.debug(status);
  228. if (!runRCmd("R -e \"find.package('rJava')\" -s")) {
  229. logger.debug("Error: Package rJava not found.");
  230. if (!runRCmd("R -e \"install.packages('rJava', repos='http://cran.stat.ucla.edu')\" -s")) {
  231. status += "Error finding and installing rJava, use install.packages('rJava') within R and reboot Bioclipse afterwards";
  232. logger.error(status);
  233. throw new FileNotFoundException(status);
  234. }
  235. }
  236. if (!runRCmd("R -e \"find.package('rj')\" -s")) {
  237. logger.debug("Error: Package rj not found.");
  238. installRj();
  239. } else {
  240. runRCmd("R -e \"installed.packages()['rj','Version']\" -s");
  241. if (!status.startsWith("[1] \"2")) {
  242. status += "Wrong 'rj' package installed, please install version 2.0";
  243. logger.error(status);
  244. if (runRCmd("R -e \"remove.packages('rj')\" -s"))
  245. installRj();
  246. }
  247. }
  248. if (!runRCmd("R -e \"find.package('bc2r')\" -s")) {
  249. String rPluginPath = null;
  250. logger.debug("Error: Package bc2r not found.");
  251. try {
  252. rPluginPath = FileUtil.getFilePath("bc2r_2.0.tar.gz", "net.bioclipse.r.business");
  253. if (OS.startsWith("Windows")) {
  254. rPluginPath = rPluginPath.substring(1).replace(fileseparator, "/");
  255. }
  256. logger.debug(rPluginPath);
  257. } catch (IllegalArgumentException e) {
  258. // TODO Auto-generated catch block
  259. e.printStackTrace();
  260. logger.debug(e.getMessage());
  261. } catch (IOException e) {
  262. // TODO Auto-generated catch block
  263. e.printStackTrace();
  264. logger.debug(e.getMessage());
  265. }
  266. if (!runRCmd("R -e \"install.packages('" + rPluginPath + "', repos= NULL, type='source')\" -s")) {
  267. status += "Error finding and installing bc2r package";
  268. logger.error(status);
  269. throw new FileNotFoundException(status);
  270. }
  271. }
  272. }
  273. /**
  274. * Gets the boolean noting if we have the right R version
  275. * @return rightRVersion boolean
  276. */
  277. public boolean getRightRVersion(){
  278. return this.rightRVersion;
  279. }
  280. private boolean installRj() throws FileNotFoundException {
  281. if (!runRCmd("R -e \"install.packages(c('rj', 'rj.gd'), repos='http://download.walware.de/rj-2.0')\" -s")) {
  282. status += "Error installing rj-package, try manually from: http://www.walware.de/it/downloads/rj.mframe";
  283. logger.error("Error: Installation of rj failed.");
  284. throw new FileNotFoundException(status);
  285. }
  286. return working;
  287. }
  288. /**
  289. * For some reason or another, on Linux, when booting R with StatET it does
  290. * not see all the same lib paths as when booted from the command line.
  291. *
  292. * Thus, this method runs <code>.libPatsh()</code> to detect a user dir
  293. * lib path, by comparing given paths to the user.home Java property,
  294. * and returns that value.
  295. *
  296. * @return null, if no user.home-based lib path is found
  297. */
  298. private String checkUserLibDir() {
  299. // user .libPaths() to list all paths known when R is run from the
  300. // command line
  301. if (!runRCmd("R -e \".libPaths()\" -s")) {
  302. logger.error("Could not detect user lib path.");
  303. } else {
  304. // split on '"'. This gives irrelevant strings, but those will
  305. // not match a user.home anyway
  306. String[] parts = status.split("\\\"");
  307. String userHome = System.getProperty("user.home");
  308. logger.debug("user.home: " + userHome);
  309. for (int i=0; i<parts.length; i++) {
  310. String part = parts[i];
  311. // The replacement for front-slashes for windows systems.
  312. if (part != null && part.startsWith(userHome) || part.replace("/", "\\").startsWith(userHome)) {
  313. // OK, we found the first lib path in $HOME
  314. return part;
  315. }
  316. }
  317. }
  318. return null;
  319. }
  320. private String checkRPath() {
  321. String rPath = "";
  322. if (OS.startsWith("Mac")) {
  323. rPath = "/usr/bin/";
  324. if (!rExist(rPath + "R")) {
  325. rPath = "/usr/local/bin/";
  326. } else if (!rExist(rPath + "R")){
  327. rPath = "";
  328. }
  329. }
  330. return rPath;
  331. }
  332. // Check if R_HOME is correctly set and tries to correct simple errors.
  333. public String checkR_HOME(String path) throws FileNotFoundException {
  334. boolean trustRPath = false;
  335. if (OS.startsWith("Mac")) {
  336. trustRPath = rExist(path + "/R") || rExist(path + "/bin/R");
  337. if(!trustRPath) {
  338. path = "/Library/Frameworks/R.framework/Resources";
  339. trustRPath = rExist(path + "/R");
  340. }
  341. if(!trustRPath) {
  342. path = "/opt/local/lib/R";
  343. trustRPath = rExist(path + "/bin/R");
  344. }
  345. } else if (OS.startsWith("Windows")) {
  346. if (path == null) {
  347. path = RegQuery("HKLM\\SOFTWARE\\R-core\\R /v InstallPath");
  348. if (path == null)
  349. path = "";
  350. }
  351. trustRPath = rExist(path + "\\bin\\R.exe");
  352. } else if (OS.startsWith("Linux")) {
  353. if (path == null) {
  354. path = "/usr/lib/R";
  355. }
  356. trustRPath = rExist(path);
  357. // link: /usr/bin/R -> /usr/lib/R/bin/R
  358. // no link: /usr/lib/R/R -> /usr/lib/R/bin/R
  359. // R_HOME is /usr/lib/R
  360. }
  361. if (!trustRPath)
  362. throw new FileNotFoundException("Incorrect R_HOME path: " + path);
  363. logger.debug("R_HOME = " + path);
  364. return path;
  365. }
  366. private Boolean rExist(String testPath) {
  367. File f = new File(testPath);
  368. return f.exists();
  369. }
  370. /**
  371. * Extract registry keys from Windows OS
  372. */
  373. private String RegQuery(String key) {
  374. final String REGQUERY_UTIL = "reg query ";
  375. final String REGSTR_TOKEN = "REG_SZ";
  376. String QUERY = REGQUERY_UTIL + key;
  377. String result;
  378. final class StreamReader extends Thread {
  379. private InputStream is;
  380. private StringWriter sw;
  381. StreamReader(InputStream is) {
  382. this.is = is;
  383. sw = new StringWriter();
  384. }
  385. public void run() {
  386. try {
  387. int c;
  388. while ((c = is.read()) != -1)
  389. sw.write(c);
  390. }
  391. catch (IOException e) { ; }
  392. }
  393. String getResult() {
  394. return sw.toString();
  395. }
  396. }
  397. try {
  398. Process process = Runtime.getRuntime().exec(QUERY);
  399. StreamReader reader = new StreamReader(process.getInputStream());
  400. reader.start();
  401. process.waitFor();
  402. reader.join();
  403. result = reader.getResult();
  404. int p = result.indexOf(REGSTR_TOKEN);
  405. if (p == -1)
  406. result = null;
  407. result = result.substring(p + REGSTR_TOKEN.length()).trim();
  408. }
  409. catch (Exception e) {
  410. logger.debug(e.getMessage());
  411. result = null;
  412. }
  413. logger.debug(result);
  414. return result;
  415. }
  416. private void initSession() {
  417. File file = new File(workspacePath.toString());
  418. if (!file.exists())
  419. file.mkdir();
  420. //Load the bc2r package
  421. eval("library(bc2r)", rservi);
  422. // Java crashes when setting working directory with "\" in windows
  423. eval("setwd(\""+file.getAbsolutePath().replace(fileseparator, "/")+"\")", rservi);
  424. status = "R workspace: " + eval("getwd()", rservi).substring(3);
  425. FilenameFilter filter = new FilenameFilter() { // Filter out the R-session files
  426. @Override
  427. public boolean accept(File dir, String name) {
  428. logger.debug(name);
  429. return name.contains(".RData");
  430. }
  431. };
  432. // Show R sessionfiles for user
  433. String[] files = file.list(filter);
  434. for(int i=0; i<files.length; i++){
  435. status += NEWLINE + "Loaded R session: " + files[i];
  436. eval("load(\"" + files[i] + "\")", rservi);
  437. }
  438. status += NEWLINE + "Use load(\"file\") and save.image(\"file\") to manage your R sessions";
  439. if (OS.startsWith("Mac")) { // the default plotting device on Mac(Quartz) is not working good with StatET
  440. eval("options(device='x11')", rservi);
  441. }
  442. }
  443. public String eval(String command, RServi myRServi) {
  444. logger.debug("R cmd: " + command);
  445. String returnVal = "R console is inactivated: " + NEWLINE + status;
  446. if (working) {
  447. if ( OS.startsWith("Mac") && command.contains("install.packages") && !command.contains("repos=")) {
  448. int i = command.lastIndexOf(")");
  449. StringBuilder cmdDefMirror = new StringBuilder(command.substring(0, i));
  450. cmdDefMirror.append(", repos=\"http://cran.us.r-project.org\")");
  451. command = cmdDefMirror.toString();
  452. }
  453. if (command.contains("quartz")) {
  454. returnVal = "quartz() is currently disabled for stability reasons" + NEWLINE + "Please use X11 for plotting!";
  455. }
  456. else if (command.contains("chooseCRANmirror") && OS.startsWith("Mac")) {
  457. returnVal = "ChooseCRANmirror is not available on Mac OS X.";
  458. }
  459. else if (command.contains("help.search()") || command.contains("??")) {
  460. returnVal = "help.search() and ?? searching is currently not supported in Bioclipse-R!";
  461. }
  462. else if (command.startsWith("help(") || command.startsWith("?")) {
  463. String hUrl = urlFromCommand(command);
  464. if (hUrl.startsWith("http://")) {
  465. returnVal = help(hUrl);
  466. } else {
  467. returnVal = hUrl;
  468. }
  469. }
  470. else try {
  471. RObject data = myRServi.evalData("capture.output("+command+")", null); // capture.output(print( )) gives a string output from R, otherwise R objects. The extra pair of () is needed for the R function print to work properly.
  472. RStore<?> rData = data.getData();
  473. StringBuilder builder = new StringBuilder();
  474. long n = rData.getLength();
  475. for(int i=0;i<n;i++) {
  476. builder.append(rData.getChar(i));
  477. if (i+1 < n)
  478. builder.append(NEWLINE);
  479. }
  480. returnVal = builder.toString();
  481. }
  482. catch (CoreException rError) { // Catch R errors.
  483. String errorStr = extractRError(rError.getMessage());
  484. if (errorStr.contains("RServi is closed")) {
  485. returnVal = "Error: " + errorStr +
  486. NEWLINE + R_CONSOLE_ERR_MESSAGE;
  487. working = false;
  488. status = R_CONSOLE_ERR_MESSAGE;
  489. } else {
  490. returnVal = "Error: " + errorStr;
  491. }
  492. }
  493. catch (Throwable error) {
  494. error.printStackTrace();
  495. returnVal = "Error: " + error.getMessage() +
  496. NEWLINE + R_CONSOLE_ERR_MESSAGE;
  497. working = false;
  498. status = R_CONSOLE_ERR_MESSAGE;
  499. }
  500. logger.debug(" -> "+ NEWLINE + returnVal);
  501. }
  502. return returnVal;
  503. }
  504. public String ls() {
  505. return eval("ls()", rservi);
  506. }
  507. /**
  508. * Opens help in browser
  509. */
  510. private String help(final String hUrl) {
  511. if (hUrl.startsWith("http://")) {
  512. Display.getDefault().asyncExec(new Runnable() {
  513. @Override
  514. public void run() {
  515. BioclipsePlatformManager bioclipse = new BioclipsePlatformManager();
  516. try {
  517. bioclipse.openURL(new URL(hUrl));
  518. } catch (MalformedURLException e) {
  519. logger.info(e);
  520. } catch (BioclipseException e) {
  521. e.printStackTrace();
  522. }
  523. }
  524. });
  525. return "";
  526. } else {
  527. return hUrl;
  528. }
  529. }
  530. public String createMatrix(String varName, IMatrixResource matrixData) {
  531. StringBuffer results = new StringBuffer();
  532. results.append(eval(
  533. "connection <- " + "textConnection(\"" +
  534. matrixAsString(matrixData) + "\")", rservi
  535. ));
  536. results.append(eval(
  537. varName + " <- read.csv(connection)", rservi
  538. ));
  539. results.append(eval(
  540. "close(connection)", rservi
  541. ));
  542. return results.toString();
  543. }
  544. private String matrixAsString(IMatrixResource matrix) {
  545. StringBuffer buffer = new StringBuffer();
  546. for (int col=1; col<=matrix.getColumnCount(); col++) {
  547. buffer.append(
  548. matrix.getColumnName(col) == null ?
  549. "X" + col : matrix.getColumnName(col)
  550. );
  551. if (col<matrix.getColumnCount()) {
  552. buffer.append(",");
  553. }
  554. }
  555. buffer.append("\n");
  556. for (int row=0; row<matrix.getRowCount(); row++) {
  557. if (matrix.getRowName(row+1) != null) {
  558. buffer.append(matrix.getRowName(row+1)).append(",");
  559. }
  560. for (int col=0; col<matrix.getColumnCount(); col++) {
  561. buffer.append(matrix.get(row+1, col+1));
  562. if (col<matrix.getColumnCount()-1) {
  563. buffer.append(",");
  564. }
  565. }
  566. buffer.append("\n");
  567. }
  568. return buffer.toString();
  569. }
  570. private String extractRError(String error) {
  571. logger.debug("full error: " + error);
  572. String result = error;
  573. if (error.startsWith("Evaluation failed")) {
  574. result = error.substring(error.indexOf(":")+1).trim();
  575. int index;
  576. if ((index=result.indexOf("):")) > 0) {
  577. result = result.substring(index+2, result.lastIndexOf(">.")).trim();
  578. }
  579. }
  580. return result;
  581. }
  582. private String urlFromCommand(String command) {
  583. String url = null;
  584. if (command.startsWith("?")) {
  585. command = command.substring(1);
  586. } else {
  587. if (command.contains("(\"")) {
  588. command = command.substring(command.indexOf("(\"") + 2, command.length() - 3);
  589. } else {
  590. command = command.substring(command.indexOf("(") + 1, command.length() - 2);
  591. }
  592. }
  593. url = eval("getHelpAddress(help("+ command +", help_type=\"html\"))", rservi);
  594. if (url.contains("http://")) {
  595. url = url.substring(5, url.length()-1);
  596. logger.debug("URL is " + url);
  597. return url;
  598. } else {
  599. return url;
  600. }
  601. }
  602. private static int compare(String v1, String v2) {
  603. String s1 = normalisedVersion(v1);
  604. String s2 = normalisedVersion(v2);
  605. int cmp = s1.compareTo(s2);
  606. String cmpStr = cmp < 0 ? "<" : cmp > 0 ? ">" : "==";
  607. System.out.printf("'%s' %s '%s'%n", v1, cmpStr, v2);
  608. return cmp;
  609. }
  610. public static String normalisedVersion(String version) {
  611. return normalisedVersion(version, ".", 4);
  612. }
  613. public static String normalisedVersion(String version, String sep, int maxWidth) {
  614. String[] split = Pattern.compile(sep, Pattern.LITERAL).split(version);
  615. StringBuilder sb = new StringBuilder();
  616. for (String s : split) {
  617. sb.append(String.format("%" + maxWidth + 's', s));
  618. }
  619. return sb.toString();
  620. }
  621. }