PageRenderTime 51ms CodeModel.GetById 10ms RepoModel.GetById 0ms app.codeStats 1ms

/projects/tomcat-7.0.2/java/org/apache/catalina/manager/HTMLManagerServlet.java

https://gitlab.com/essere.lab.public/qualitas.class-corpus
Java | 1255 lines | 873 code | 127 blank | 255 comment | 173 complexity | e64ea637e0fa53192c577fc785ac3477 MD5 | raw file
  1. /*
  2. * Licensed to the Apache Software Foundation (ASF) under one or more
  3. * contributor license agreements. See the NOTICE file distributed with
  4. * this work for additional information regarding copyright ownership.
  5. * The ASF licenses this file to You under the Apache License, Version 2.0
  6. * (the "License"); you may not use this file except in compliance with
  7. * the License. You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. */
  17. package org.apache.catalina.manager;
  18. import java.io.File;
  19. import java.io.IOException;
  20. import java.io.PrintWriter;
  21. import java.io.StringWriter;
  22. import java.text.MessageFormat;
  23. import java.util.ArrayList;
  24. import java.util.Arrays;
  25. import java.util.Collection;
  26. import java.util.Collections;
  27. import java.util.Comparator;
  28. import java.util.Date;
  29. import java.util.Iterator;
  30. import java.util.List;
  31. import java.util.Locale;
  32. import java.util.Map;
  33. import java.util.Random;
  34. import java.util.Set;
  35. import java.util.TreeMap;
  36. import javax.servlet.ServletException;
  37. import javax.servlet.http.HttpServletRequest;
  38. import javax.servlet.http.HttpServletResponse;
  39. import javax.servlet.http.HttpSession;
  40. import javax.servlet.http.Part;
  41. import org.apache.catalina.Container;
  42. import org.apache.catalina.Context;
  43. import org.apache.catalina.Manager;
  44. import org.apache.catalina.Session;
  45. import org.apache.catalina.ha.session.BackupManager;
  46. import org.apache.catalina.manager.util.BaseSessionComparator;
  47. import org.apache.catalina.manager.util.ReverseComparator;
  48. import org.apache.catalina.manager.util.SessionUtils;
  49. import org.apache.catalina.util.RequestUtil;
  50. import org.apache.catalina.util.ServerInfo;
  51. import org.apache.catalina.util.URLEncoder;
  52. import org.apache.tomcat.util.ExceptionUtils;
  53. import org.apache.tomcat.util.http.fileupload.ParameterParser;
  54. /**
  55. * Servlet that enables remote management of the web applications deployed
  56. * within the same virtual host as this web application is. Normally, this
  57. * functionality will be protected by a security constraint in the web
  58. * application deployment descriptor. However, this requirement can be
  59. * relaxed during testing.
  60. * <p>
  61. * The difference between the <code>ManagerServlet</code> and this
  62. * Servlet is that this Servlet prints out a HTML interface which
  63. * makes it easier to administrate.
  64. * <p>
  65. * However if you use a software that parses the output of
  66. * <code>ManagerServlet</code> you won't be able to upgrade
  67. * to this Servlet since the output are not in the
  68. * same format ar from <code>ManagerServlet</code>
  69. *
  70. * @author Bip Thelin
  71. * @author Malcolm Edgar
  72. * @author Glenn L. Nielsen
  73. * @version $Id: HTMLManagerServlet.java 980567 2010-07-29 20:53:26Z markt $
  74. * @see ManagerServlet
  75. */
  76. public final class HTMLManagerServlet extends ManagerServlet {
  77. private static final long serialVersionUID = 1L;
  78. protected static final URLEncoder URL_ENCODER;
  79. protected static final String APPLICATION_MESSAGE = "message";
  80. protected static final String APPLICATION_ERROR = "error";
  81. protected static final String sessionsListJspPath = "/WEB-INF/jsp/sessionsList.jsp";
  82. protected static final String sessionDetailJspPath = "/WEB-INF/jsp/sessionDetail.jsp";
  83. static {
  84. URL_ENCODER = new URLEncoder();
  85. // '/' should not be encoded in context paths
  86. URL_ENCODER.addSafeCharacter('/');
  87. }
  88. private final Random randomSource = new Random();
  89. private boolean showProxySessions = false;
  90. // --------------------------------------------------------- Public Methods
  91. /**
  92. * Process a GET request for the specified resource.
  93. *
  94. * @param request The servlet request we are processing
  95. * @param response The servlet response we are creating
  96. *
  97. * @exception IOException if an input/output error occurs
  98. * @exception ServletException if a servlet-specified error occurs
  99. */
  100. @Override
  101. public void doGet(HttpServletRequest request,
  102. HttpServletResponse response)
  103. throws IOException, ServletException {
  104. // Identify the request parameters that we need
  105. // By obtaining the command from the pathInfo, per-command security can
  106. // be configured in web.xml
  107. String command = request.getPathInfo();
  108. String path = request.getParameter("path");
  109. // Prepare our output writer to generate the response message
  110. response.setContentType("text/html; charset=" + Constants.CHARSET);
  111. String message = "";
  112. // Process the requested command
  113. if (command == null || command.equals("/")) {
  114. // No command == list
  115. } else if (command.equals("/list")) {
  116. // List always displayed - nothing to do here
  117. } else if (command.equals("/sessions")) {
  118. try {
  119. doSessions(path, request, response);
  120. return;
  121. } catch (Exception e) {
  122. log("HTMLManagerServlet.sessions[" + path + "]", e);
  123. message = sm.getString("managerServlet.exception",
  124. e.toString());
  125. }
  126. } else if (command.equals("/upload") || command.equals("/deploy") ||
  127. command.equals("/reload") || command.equals("/undeploy") ||
  128. command.equals("/expire") || command.equals("/start") ||
  129. command.equals("/stop")) {
  130. message =
  131. sm.getString("managerServlet.postCommand", command);
  132. } else {
  133. message =
  134. sm.getString("managerServlet.unknownCommand", command);
  135. }
  136. list(request, response, message);
  137. }
  138. /**
  139. * Process a POST request for the specified resource.
  140. *
  141. * @param request The servlet request we are processing
  142. * @param response The servlet response we are creating
  143. *
  144. * @exception IOException if an input/output error occurs
  145. * @exception ServletException if a servlet-specified error occurs
  146. */
  147. @Override
  148. public void doPost(HttpServletRequest request,
  149. HttpServletResponse response)
  150. throws IOException, ServletException {
  151. // Identify the request parameters that we need
  152. // By obtaining the command from the pathInfo, per-command security can
  153. // be configured in web.xml
  154. String command = request.getPathInfo();
  155. String path = request.getParameter("path");
  156. String deployPath = request.getParameter("deployPath");
  157. String deployConfig = request.getParameter("deployConfig");
  158. String deployWar = request.getParameter("deployWar");
  159. // Prepare our output writer to generate the response message
  160. response.setContentType("text/html; charset=" + Constants.CHARSET);
  161. String message = "";
  162. if (command == null || command.length() == 0) {
  163. // No command == list
  164. // List always displayed -> do nothing
  165. } else if (command.equals("/upload")) {
  166. message = upload(request);
  167. } else if (command.equals("/deploy")) {
  168. message = deployInternal(deployConfig, deployPath, deployWar);
  169. } else if (command.equals("/reload")) {
  170. message = reload(path);
  171. } else if (command.equals("/undeploy")) {
  172. message = undeploy(path);
  173. } else if (command.equals("/expire")) {
  174. message = expireSessions(path, request);
  175. } else if (command.equals("/start")) {
  176. message = start(path);
  177. } else if (command.equals("/stop")) {
  178. message = stop(path);
  179. } else if (command.equals("/findleaks")) {
  180. message = findleaks();
  181. } else {
  182. // Try GET
  183. doGet(request,response);
  184. return;
  185. }
  186. list(request, response, message);
  187. }
  188. /**
  189. * Generate a once time token (nonce) for authenticating subsequent
  190. * requests. This will also add the token to the session. The nonce
  191. * generation is a simplified version of ManagerBase.generateSessionId().
  192. *
  193. */
  194. protected String generateNonce() {
  195. byte random[] = new byte[16];
  196. // Render the result as a String of hexadecimal digits
  197. StringBuilder buffer = new StringBuilder();
  198. randomSource.nextBytes(random);
  199. for (int j = 0; j < random.length; j++) {
  200. byte b1 = (byte) ((random[j] & 0xf0) >> 4);
  201. byte b2 = (byte) (random[j] & 0x0f);
  202. if (b1 < 10)
  203. buffer.append((char) ('0' + b1));
  204. else
  205. buffer.append((char) ('A' + (b1 - 10)));
  206. if (b2 < 10)
  207. buffer.append((char) ('0' + b2));
  208. else
  209. buffer.append((char) ('A' + (b2 - 10)));
  210. }
  211. return buffer.toString();
  212. }
  213. protected String upload(HttpServletRequest request)
  214. throws IOException, ServletException {
  215. String message = "";
  216. Part warPart = null;
  217. String filename = null;
  218. String basename = null;
  219. Collection<Part> parts = request.getParts();
  220. Iterator<Part> iter = parts.iterator();
  221. try {
  222. while (iter.hasNext()) {
  223. Part part = iter.next();
  224. if (part.getName().equals("deployWar") && warPart == null) {
  225. warPart = part;
  226. } else {
  227. part.delete();
  228. }
  229. }
  230. while (true) {
  231. if (warPart == null) {
  232. message =
  233. sm.getString("htmlManagerServlet.deployUploadNoFile");
  234. break;
  235. }
  236. filename =
  237. extractFilename(warPart.getHeader("Content-Disposition"));
  238. if (!filename.toLowerCase(Locale.ENGLISH).endsWith(".war")) {
  239. message = sm.getString(
  240. "htmlManagerServlet.deployUploadNotWar", filename);
  241. break;
  242. }
  243. // Get the filename if uploaded name includes a path
  244. if (filename.lastIndexOf('\\') >= 0) {
  245. filename =
  246. filename.substring(filename.lastIndexOf('\\') + 1);
  247. }
  248. if (filename.lastIndexOf('/') >= 0) {
  249. filename =
  250. filename.substring(filename.lastIndexOf('/') + 1);
  251. }
  252. // Identify the appBase of the owning Host of this Context
  253. // (if any)
  254. basename = filename.substring(0,
  255. filename.toLowerCase(Locale.ENGLISH).indexOf(".war"));
  256. File file = new File(getAppBase(), filename);
  257. if (file.exists()) {
  258. message = sm.getString(
  259. "htmlManagerServlet.deployUploadWarExists",
  260. filename);
  261. break;
  262. }
  263. String path = null;
  264. if (basename.equals("ROOT")) {
  265. path = "";
  266. } else {
  267. path = "/" + basename.replace('#', '/');
  268. }
  269. if ((host.findChild(path) != null) && !isDeployed(path)) {
  270. message = sm.getString(
  271. "htmlManagerServlet.deployUploadInServerXml",
  272. filename);
  273. break;
  274. }
  275. if (!isServiced(path)) {
  276. addServiced(path);
  277. try {
  278. warPart.write(file.getAbsolutePath());
  279. // Perform new deployment
  280. check(path);
  281. } finally {
  282. removeServiced(path);
  283. }
  284. }
  285. break;
  286. }
  287. } catch(Exception e) {
  288. message = sm.getString
  289. ("htmlManagerServlet.deployUploadFail", e.getMessage());
  290. log(message, e);
  291. } finally {
  292. if (warPart != null) {
  293. warPart.delete();
  294. }
  295. warPart = null;
  296. }
  297. return message;
  298. }
  299. /*
  300. * Adapted from FileUploadBase.getFileName()
  301. */
  302. private String extractFilename(String cd) {
  303. String fileName = null;
  304. if (cd != null) {
  305. String cdl = cd.toLowerCase(Locale.ENGLISH);
  306. if (cdl.startsWith("form-data") || cdl.startsWith("attachment")) {
  307. ParameterParser parser = new ParameterParser();
  308. parser.setLowerCaseNames(true);
  309. // Parameter parser can handle null input
  310. Map<String,String> params =
  311. parser.parse(cd, ';');
  312. if (params.containsKey("filename")) {
  313. fileName = params.get("filename");
  314. if (fileName != null) {
  315. fileName = fileName.trim();
  316. } else {
  317. // Even if there is no value, the parameter is present,
  318. // so we return an empty file name rather than no file
  319. // name.
  320. fileName = "";
  321. }
  322. }
  323. }
  324. }
  325. return fileName;
  326. }
  327. /**
  328. * Deploy an application for the specified path from the specified
  329. * web application archive.
  330. *
  331. * @param config URL of the context configuration file to be deployed
  332. * @param path Context path of the application to be deployed
  333. * @param war URL of the web application archive to be deployed
  334. * @return message String
  335. */
  336. protected String deployInternal(String config, String path, String war) {
  337. StringWriter stringWriter = new StringWriter();
  338. PrintWriter printWriter = new PrintWriter(stringWriter);
  339. super.deploy(printWriter, config, path, war, false);
  340. return stringWriter.toString();
  341. }
  342. /**
  343. * Render a HTML list of the currently active Contexts in our virtual host,
  344. * and memory and server status information.
  345. *
  346. * @param request The request
  347. * @param response The response
  348. * @param message a message to display
  349. */
  350. public void list(HttpServletRequest request,
  351. HttpServletResponse response,
  352. String message) throws IOException {
  353. if (debug >= 1)
  354. log("list: Listing contexts for virtual host '" +
  355. host.getName() + "'");
  356. PrintWriter writer = response.getWriter();
  357. // HTML Header Section
  358. writer.print(Constants.HTML_HEADER_SECTION);
  359. // Body Header Section
  360. Object[] args = new Object[2];
  361. args[0] = request.getContextPath();
  362. args[1] = sm.getString("htmlManagerServlet.title");
  363. writer.print(MessageFormat.format
  364. (Constants.BODY_HEADER_SECTION, args));
  365. // Message Section
  366. args = new Object[3];
  367. args[0] = sm.getString("htmlManagerServlet.messageLabel");
  368. if (message == null || message.length() == 0) {
  369. args[1] = "OK";
  370. } else {
  371. args[1] = RequestUtil.filter(message);
  372. }
  373. writer.print(MessageFormat.format(Constants.MESSAGE_SECTION, args));
  374. // Manager Section
  375. args = new Object[9];
  376. args[0] = sm.getString("htmlManagerServlet.manager");
  377. args[1] = response.encodeURL(request.getContextPath() + "/html/list");
  378. args[2] = sm.getString("htmlManagerServlet.list");
  379. args[3] = response.encodeURL
  380. (request.getContextPath() + "/" +
  381. sm.getString("htmlManagerServlet.helpHtmlManagerFile"));
  382. args[4] = sm.getString("htmlManagerServlet.helpHtmlManager");
  383. args[5] = response.encodeURL
  384. (request.getContextPath() + "/" +
  385. sm.getString("htmlManagerServlet.helpManagerFile"));
  386. args[6] = sm.getString("htmlManagerServlet.helpManager");
  387. args[7] = response.encodeURL
  388. (request.getContextPath() + "/status");
  389. args[8] = sm.getString("statusServlet.title");
  390. writer.print(MessageFormat.format(Constants.MANAGER_SECTION, args));
  391. // Apps Header Section
  392. args = new Object[6];
  393. args[0] = sm.getString("htmlManagerServlet.appsTitle");
  394. args[1] = sm.getString("htmlManagerServlet.appsPath");
  395. args[2] = sm.getString("htmlManagerServlet.appsName");
  396. args[3] = sm.getString("htmlManagerServlet.appsAvailable");
  397. args[4] = sm.getString("htmlManagerServlet.appsSessions");
  398. args[5] = sm.getString("htmlManagerServlet.appsTasks");
  399. writer.print(MessageFormat.format(APPS_HEADER_SECTION, args));
  400. // Apps Row Section
  401. // Create sorted map of deployed applications context paths.
  402. Container children[] = host.findChildren();
  403. String contextPaths[] = new String[children.length];
  404. for (int i = 0; i < children.length; i++)
  405. contextPaths[i] = children[i].getName();
  406. TreeMap<String,String> sortedContextPathsMap =
  407. new TreeMap<String,String>();
  408. for (int i = 0; i < contextPaths.length; i++) {
  409. String displayPath = contextPaths[i];
  410. sortedContextPathsMap.put(displayPath, contextPaths[i]);
  411. }
  412. String appsStart = sm.getString("htmlManagerServlet.appsStart");
  413. String appsStop = sm.getString("htmlManagerServlet.appsStop");
  414. String appsReload = sm.getString("htmlManagerServlet.appsReload");
  415. String appsUndeploy = sm.getString("htmlManagerServlet.appsUndeploy");
  416. String appsExpire = sm.getString("htmlManagerServlet.appsExpire");
  417. Iterator<Map.Entry<String,String>> iterator =
  418. sortedContextPathsMap.entrySet().iterator();
  419. boolean isHighlighted = true;
  420. boolean isDeployed = true;
  421. String highlightColor = null;
  422. while (iterator.hasNext()) {
  423. // Bugzilla 34818, alternating row colors
  424. isHighlighted = !isHighlighted;
  425. if(isHighlighted) {
  426. highlightColor = "#C3F3C3";
  427. } else {
  428. highlightColor = "#FFFFFF";
  429. }
  430. Map.Entry<String,String> entry = iterator.next();
  431. String displayPath = entry.getKey();
  432. String contextPath = entry.getValue();
  433. Context ctxt = (Context) host.findChild(contextPath);
  434. if (displayPath.equals("")) {
  435. displayPath = "/";
  436. }
  437. if (ctxt != null ) {
  438. try {
  439. isDeployed = isDeployed(contextPath);
  440. } catch (Exception e) {
  441. // Assume false on failure for safety
  442. isDeployed = false;
  443. }
  444. args = new Object[7];
  445. args[0] = URL_ENCODER.encode(displayPath);
  446. args[1] = displayPath;
  447. args[2] = ctxt.getDisplayName();
  448. if (args[2] == null) {
  449. args[2] = "&nbsp;";
  450. }
  451. args[3] = new Boolean(ctxt.getAvailable());
  452. args[4] = response.encodeURL
  453. (request.getContextPath() +
  454. "/html/sessions?path=" + URL_ENCODER.encode(displayPath));
  455. Manager manager = ctxt.getManager();
  456. if (manager instanceof BackupManager && showProxySessions) {
  457. args[5] = new Integer(
  458. ((BackupManager)manager).getActiveSessionsFull());
  459. } else if (ctxt.getManager() != null){
  460. args[5] = new Integer(manager.getActiveSessions());
  461. } else {
  462. args[5] = new Integer(0);
  463. }
  464. args[6] = highlightColor;
  465. writer.print
  466. (MessageFormat.format(APPS_ROW_DETAILS_SECTION, args));
  467. args = new Object[14];
  468. args[0] = response.encodeURL
  469. (request.getContextPath() +
  470. "/html/start?path=" + URL_ENCODER.encode(displayPath));
  471. args[1] = appsStart;
  472. args[2] = response.encodeURL
  473. (request.getContextPath() +
  474. "/html/stop?path=" + URL_ENCODER.encode(displayPath));
  475. args[3] = appsStop;
  476. args[4] = response.encodeURL
  477. (request.getContextPath() +
  478. "/html/reload?path=" + URL_ENCODER.encode(displayPath));
  479. args[5] = appsReload;
  480. args[6] = response.encodeURL
  481. (request.getContextPath() +
  482. "/html/undeploy?path=" + URL_ENCODER.encode(displayPath));
  483. args[7] = appsUndeploy;
  484. args[8] = response.encodeURL
  485. (request.getContextPath() +
  486. "/html/expire?path=" + URL_ENCODER.encode(displayPath));
  487. args[9] = appsExpire;
  488. args[10] = sm.getString("htmlManagerServlet.expire.explain");
  489. if (manager == null) {
  490. args[11] = sm.getString("htmlManagerServlet.noManager");
  491. } else {
  492. args[11] = new Integer(
  493. ctxt.getManager().getMaxInactiveInterval()/60);
  494. }
  495. args[12] = sm.getString("htmlManagerServlet.expire.unit");
  496. args[13] = highlightColor;
  497. if (ctxt.getPath().equals(this.context.getPath())) {
  498. writer.print(MessageFormat.format(
  499. MANAGER_APP_ROW_BUTTON_SECTION, args));
  500. } else if (ctxt.getAvailable() && isDeployed) {
  501. writer.print(MessageFormat.format(
  502. STARTED_DEPLOYED_APPS_ROW_BUTTON_SECTION, args));
  503. } else if (ctxt.getAvailable() && !isDeployed) {
  504. writer.print(MessageFormat.format(
  505. STARTED_NONDEPLOYED_APPS_ROW_BUTTON_SECTION, args));
  506. } else if (!ctxt.getAvailable() && isDeployed) {
  507. writer.print(MessageFormat.format(
  508. STOPPED_DEPLOYED_APPS_ROW_BUTTON_SECTION, args));
  509. } else {
  510. writer.print(MessageFormat.format(
  511. STOPPED_NONDEPLOYED_APPS_ROW_BUTTON_SECTION, args));
  512. }
  513. }
  514. }
  515. // Deploy Section
  516. args = new Object[7];
  517. args[0] = sm.getString("htmlManagerServlet.deployTitle");
  518. args[1] = sm.getString("htmlManagerServlet.deployServer");
  519. args[2] = response.encodeURL(request.getContextPath() + "/html/deploy");
  520. args[3] = sm.getString("htmlManagerServlet.deployPath");
  521. args[4] = sm.getString("htmlManagerServlet.deployConfig");
  522. args[5] = sm.getString("htmlManagerServlet.deployWar");
  523. args[6] = sm.getString("htmlManagerServlet.deployButton");
  524. writer.print(MessageFormat.format(DEPLOY_SECTION, args));
  525. args = new Object[4];
  526. args[0] = sm.getString("htmlManagerServlet.deployUpload");
  527. args[1] = response.encodeURL(request.getContextPath() + "/html/upload");
  528. args[2] = sm.getString("htmlManagerServlet.deployUploadFile");
  529. args[3] = sm.getString("htmlManagerServlet.deployButton");
  530. writer.print(MessageFormat.format(UPLOAD_SECTION, args));
  531. // Diagnostics section
  532. args = new Object[5];
  533. args[0] = sm.getString("htmlManagerServlet.diagnosticsTitle");
  534. args[1] = sm.getString("htmlManagerServlet.diagnosticsLeak");
  535. args[2] = response.encodeURL(
  536. request.getContextPath() + "/html/findleaks");
  537. args[3] = sm.getString("htmlManagerServlet.diagnosticsLeakWarning");
  538. args[4] = sm.getString("htmlManagerServlet.diagnosticsLeakButton");
  539. writer.print(MessageFormat.format(DIAGNOSTICS_SECTION, args));
  540. // Server Header Section
  541. args = new Object[7];
  542. args[0] = sm.getString("htmlManagerServlet.serverTitle");
  543. args[1] = sm.getString("htmlManagerServlet.serverVersion");
  544. args[2] = sm.getString("htmlManagerServlet.serverJVMVersion");
  545. args[3] = sm.getString("htmlManagerServlet.serverJVMVendor");
  546. args[4] = sm.getString("htmlManagerServlet.serverOSName");
  547. args[5] = sm.getString("htmlManagerServlet.serverOSVersion");
  548. args[6] = sm.getString("htmlManagerServlet.serverOSArch");
  549. writer.print(MessageFormat.format
  550. (Constants.SERVER_HEADER_SECTION, args));
  551. // Server Row Section
  552. args = new Object[6];
  553. args[0] = ServerInfo.getServerInfo();
  554. args[1] = System.getProperty("java.runtime.version");
  555. args[2] = System.getProperty("java.vm.vendor");
  556. args[3] = System.getProperty("os.name");
  557. args[4] = System.getProperty("os.version");
  558. args[5] = System.getProperty("os.arch");
  559. writer.print(MessageFormat.format(Constants.SERVER_ROW_SECTION, args));
  560. // HTML Tail Section
  561. writer.print(Constants.HTML_TAIL_SECTION);
  562. // Finish up the response
  563. writer.flush();
  564. writer.close();
  565. }
  566. /**
  567. * Reload the web application at the specified context path.
  568. *
  569. * @see ManagerServlet#reload(PrintWriter, String)
  570. *
  571. * @param path Context path of the application to be restarted
  572. * @return message String
  573. */
  574. protected String reload(String path) {
  575. StringWriter stringWriter = new StringWriter();
  576. PrintWriter printWriter = new PrintWriter(stringWriter);
  577. super.reload(printWriter, path);
  578. return stringWriter.toString();
  579. }
  580. /**
  581. * Undeploy the web application at the specified context path.
  582. *
  583. * @see ManagerServlet#undeploy(PrintWriter, String)
  584. *
  585. * @param path Context path of the application to be undeployed
  586. * @return message String
  587. */
  588. protected String undeploy(String path) {
  589. StringWriter stringWriter = new StringWriter();
  590. PrintWriter printWriter = new PrintWriter(stringWriter);
  591. super.undeploy(printWriter, path);
  592. return stringWriter.toString();
  593. }
  594. /**
  595. * Display session information and invoke list.
  596. *
  597. * @see ManagerServlet#sessions(PrintWriter, String, int)
  598. *
  599. * @param path Context path of the application to list session information
  600. * @param idle Expire all sessions with idle time &ge; idle for this context
  601. * @return message String
  602. */
  603. public String sessions(String path, int idle) {
  604. StringWriter stringWriter = new StringWriter();
  605. PrintWriter printWriter = new PrintWriter(stringWriter);
  606. super.sessions(printWriter, path, idle);
  607. return stringWriter.toString();
  608. }
  609. /**
  610. * Display session information and invoke list.
  611. *
  612. * @see ManagerServlet#sessions(PrintWriter, String)
  613. *
  614. * @param path Context path of the application to list session information
  615. * @return message String
  616. */
  617. public String sessions(String path) {
  618. return sessions(path, -1);
  619. }
  620. /**
  621. * Start the web application at the specified context path.
  622. *
  623. * @see ManagerServlet#start(PrintWriter, String)
  624. *
  625. * @param path Context path of the application to be started
  626. * @return message String
  627. */
  628. public String start(String path) {
  629. StringWriter stringWriter = new StringWriter();
  630. PrintWriter printWriter = new PrintWriter(stringWriter);
  631. super.start(printWriter, path);
  632. return stringWriter.toString();
  633. }
  634. /**
  635. * Stop the web application at the specified context path.
  636. *
  637. * @see ManagerServlet#stop(PrintWriter, String)
  638. *
  639. * @param path Context path of the application to be stopped
  640. * @return message String
  641. */
  642. protected String stop(String path) {
  643. StringWriter stringWriter = new StringWriter();
  644. PrintWriter printWriter = new PrintWriter(stringWriter);
  645. super.stop(printWriter, path);
  646. return stringWriter.toString();
  647. }
  648. /**
  649. * Find potential memory leaks caused by web application reload.
  650. *
  651. * @see ManagerServlet#findleaks(PrintWriter)
  652. *
  653. * @return message String
  654. */
  655. protected String findleaks() {
  656. StringBuilder msg = new StringBuilder();
  657. StringWriter stringWriter = new StringWriter();
  658. PrintWriter printWriter = new PrintWriter(stringWriter);
  659. super.findleaks(printWriter);
  660. if (stringWriter.getBuffer().length() > 0) {
  661. msg.append(sm.getString("htmlManagerServlet.findleaksList"));
  662. msg.append(stringWriter.toString());
  663. } else {
  664. msg.append(sm.getString("htmlManagerServlet.findleaksNone"));
  665. }
  666. return msg.toString();
  667. }
  668. /**
  669. * @see javax.servlet.Servlet#getServletInfo()
  670. */
  671. @Override
  672. public String getServletInfo() {
  673. return "HTMLManagerServlet, Copyright (c) 1999-2010, The Apache Software Foundation";
  674. }
  675. /**
  676. * @see javax.servlet.GenericServlet#init()
  677. */
  678. @Override
  679. public void init() throws ServletException {
  680. super.init();
  681. // Set our properties from the initialization parameters
  682. String value = null;
  683. try {
  684. value = getServletConfig().getInitParameter("showProxySessions");
  685. showProxySessions = Boolean.parseBoolean(value);
  686. } catch (Throwable t) {
  687. ExceptionUtils.handleThrowable(t);
  688. }
  689. }
  690. // ------------------------------------------------ Sessions administration
  691. /**
  692. *
  693. * Extract the expiration request parameter
  694. *
  695. * @param path
  696. * @param req
  697. */
  698. protected String expireSessions(String path, HttpServletRequest req) {
  699. int idle = -1;
  700. String idleParam = req.getParameter("idle");
  701. if (idleParam != null) {
  702. try {
  703. idle = Integer.parseInt(idleParam);
  704. } catch (NumberFormatException e) {
  705. log("Could not parse idle parameter to an int: " + idleParam);
  706. }
  707. }
  708. return sessions(path, idle);
  709. }
  710. /**
  711. *
  712. * @param req
  713. * @param resp
  714. * @throws ServletException
  715. * @throws IOException
  716. */
  717. protected void doSessions(String path, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  718. req.setAttribute("path", path);
  719. String action = req.getParameter("action");
  720. if (debug >= 1) {
  721. log("sessions: Session action '" + action + "' for web application at '" + path + "'");
  722. }
  723. if ("sessionDetail".equals(action)) {
  724. String sessionId = req.getParameter("sessionId");
  725. displaySessionDetailPage(req, resp, path, sessionId);
  726. return;
  727. } else if ("invalidateSessions".equals(action)) {
  728. String[] sessionIds = req.getParameterValues("sessionIds");
  729. int i = invalidateSessions(path, sessionIds);
  730. req.setAttribute(APPLICATION_MESSAGE, "" + i + " sessions invalidated.");
  731. } else if ("removeSessionAttribute".equals(action)) {
  732. String sessionId = req.getParameter("sessionId");
  733. String name = req.getParameter("attributeName");
  734. boolean removed = removeSessionAttribute(path, sessionId, name);
  735. String outMessage = removed ? "Session attribute '" + name + "' removed." : "Session did not contain any attribute named '" + name + "'";
  736. req.setAttribute(APPLICATION_MESSAGE, outMessage);
  737. resp.sendRedirect(resp.encodeRedirectURL(req.getRequestURL().append("?path=").append(path).append("&action=sessionDetail&sessionId=").append(sessionId).toString()));
  738. return;
  739. } // else
  740. displaySessionsListPage(path, req, resp);
  741. }
  742. protected List<Session> getSessionsForPath(String path) {
  743. if ((path == null) || (!path.startsWith("/") && path.equals(""))) {
  744. throw new IllegalArgumentException(sm.getString("managerServlet.invalidPath",
  745. RequestUtil.filter(path)));
  746. }
  747. String searchPath = path;
  748. if( path.equals("/") )
  749. searchPath = "";
  750. Context ctxt = (Context) host.findChild(searchPath);
  751. if (null == ctxt) {
  752. throw new IllegalArgumentException(sm.getString("managerServlet.noContext",
  753. RequestUtil.filter(path)));
  754. }
  755. Manager manager = ctxt.getManager();
  756. List<Session> sessions = new ArrayList<Session>();
  757. sessions.addAll(Arrays.asList(manager.findSessions()));
  758. if (manager instanceof BackupManager && showProxySessions) {
  759. // Add dummy proxy sessions
  760. Set<String> sessionIds =
  761. ((BackupManager) manager).getSessionIdsFull();
  762. // Remove active (primary and backup) session IDs from full list
  763. for (Session session : sessions) {
  764. sessionIds.remove(session.getId());
  765. }
  766. // Left with just proxy sessions - add them
  767. for (String sessionId : sessionIds) {
  768. sessions.add(new DummyProxySession(sessionId));
  769. }
  770. }
  771. return sessions;
  772. }
  773. protected Session getSessionForPathAndId(String path, String id) throws IOException {
  774. if ((path == null) || (!path.startsWith("/") && path.equals(""))) {
  775. throw new IllegalArgumentException(sm.getString("managerServlet.invalidPath",
  776. RequestUtil.filter(path)));
  777. }
  778. String searchPath = path;
  779. if( path.equals("/") )
  780. searchPath = "";
  781. Context ctxt = (Context) host.findChild(searchPath);
  782. if (null == ctxt) {
  783. throw new IllegalArgumentException(sm.getString("managerServlet.noContext",
  784. RequestUtil.filter(path)));
  785. }
  786. Session session = ctxt.getManager().findSession(id);
  787. return session;
  788. }
  789. /**
  790. *
  791. * @param req
  792. * @param resp
  793. * @throws ServletException
  794. * @throws IOException
  795. */
  796. protected void displaySessionsListPage(String path, HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
  797. List<Session> sessions = getSessionsForPath(path);
  798. String sortBy = req.getParameter("sort");
  799. String orderBy = null;
  800. if (null != sortBy && !"".equals(sortBy.trim())) {
  801. Comparator<Session> comparator = getComparator(sortBy);
  802. if (comparator != null) {
  803. orderBy = req.getParameter("order");
  804. if ("DESC".equalsIgnoreCase(orderBy)) {
  805. comparator = new ReverseComparator(comparator);
  806. // orderBy = "ASC";
  807. } else {
  808. //orderBy = "DESC";
  809. }
  810. try {
  811. Collections.sort(sessions, comparator);
  812. } catch (IllegalStateException ise) {
  813. // at least 1 of the sessions is invalidated
  814. req.setAttribute(APPLICATION_ERROR, "Can't sort session list: one session is invalidated");
  815. }
  816. } else {
  817. log("WARNING: unknown sort order: " + sortBy);
  818. }
  819. }
  820. // keep sort order
  821. req.setAttribute("sort", sortBy);
  822. req.setAttribute("order", orderBy);
  823. req.setAttribute("activeSessions", sessions);
  824. //strong>NOTE</strong> - This header will be overridden
  825. // automatically if a <code>RequestDispatcher.forward()</code> call is
  826. // ultimately invoked.
  827. resp.setHeader("Pragma", "No-cache"); // HTTP 1.0
  828. resp.setHeader("Cache-Control", "no-cache,no-store,max-age=0"); // HTTP 1.1
  829. resp.setDateHeader("Expires", 0); // 0 means now
  830. getServletContext().getRequestDispatcher(sessionsListJspPath).include(req, resp);
  831. }
  832. /**
  833. *
  834. * @param req
  835. * @param resp
  836. * @throws ServletException
  837. * @throws IOException
  838. */
  839. protected void displaySessionDetailPage(HttpServletRequest req, HttpServletResponse resp, String path, String sessionId) throws ServletException, IOException {
  840. Session session = getSessionForPathAndId(path, sessionId);
  841. //strong>NOTE</strong> - This header will be overridden
  842. // automatically if a <code>RequestDispatcher.forward()</code> call is
  843. // ultimately invoked.
  844. resp.setHeader("Pragma", "No-cache"); // HTTP 1.0
  845. resp.setHeader("Cache-Control", "no-cache,no-store,max-age=0"); // HTTP 1.1
  846. resp.setDateHeader("Expires", 0); // 0 means now
  847. req.setAttribute("currentSession", session);
  848. getServletContext().getRequestDispatcher(resp.encodeURL(sessionDetailJspPath)).include(req, resp);
  849. }
  850. /**
  851. * Invalidate HttpSessions
  852. * @param sessionIds
  853. * @return number of invalidated sessions
  854. * @throws IOException
  855. */
  856. public int invalidateSessions(String path, String[] sessionIds) throws IOException {
  857. if (null == sessionIds) {
  858. return 0;
  859. }
  860. int nbAffectedSessions = 0;
  861. for (int i = 0; i < sessionIds.length; ++i) {
  862. String sessionId = sessionIds[i];
  863. HttpSession session = getSessionForPathAndId(path, sessionId).getSession();
  864. if (null == session) {
  865. // Shouldn't happen, but let's play nice...
  866. if (debug >= 1) {
  867. log("WARNING: can't invalidate null session " + sessionId);
  868. }
  869. continue;
  870. }
  871. try {
  872. session.invalidate();
  873. ++nbAffectedSessions;
  874. if (debug >= 1) {
  875. log("Invalidating session id " + sessionId);
  876. }
  877. } catch (IllegalStateException ise) {
  878. if (debug >= 1) {
  879. log("Can't invalidate already invalidated session id " + sessionId);
  880. }
  881. }
  882. }
  883. return nbAffectedSessions;
  884. }
  885. /**
  886. * Removes an attribute from an HttpSession
  887. * @param sessionId
  888. * @param attributeName
  889. * @return true if there was an attribute removed, false otherwise
  890. * @throws IOException
  891. */
  892. public boolean removeSessionAttribute(String path, String sessionId, String attributeName) throws IOException {
  893. HttpSession session = getSessionForPathAndId(path, sessionId).getSession();
  894. if (null == session) {
  895. // Shouldn't happen, but let's play nice...
  896. if (debug >= 1) {
  897. log("WARNING: can't remove attribute '" + attributeName + "' for null session " + sessionId);
  898. }
  899. return false;
  900. }
  901. boolean wasPresent = (null != session.getAttribute(attributeName));
  902. try {
  903. session.removeAttribute(attributeName);
  904. } catch (IllegalStateException ise) {
  905. if (debug >= 1) {
  906. log("Can't remote attribute '" + attributeName + "' for invalidated session id " + sessionId);
  907. }
  908. }
  909. return wasPresent;
  910. }
  911. /**
  912. * Sets the maximum inactive interval (session timeout) an HttpSession
  913. * @param sessionId
  914. * @param maxInactiveInterval in seconds
  915. * @return old value for maxInactiveInterval
  916. * @throws IOException
  917. */
  918. public int setSessionMaxInactiveInterval(String path, String sessionId, int maxInactiveInterval) throws IOException {
  919. HttpSession session = getSessionForPathAndId(path, sessionId).getSession();
  920. if (null == session) {
  921. // Shouldn't happen, but let's play nice...
  922. if (debug >= 1) {
  923. log("WARNING: can't set timout for null session " + sessionId);
  924. }
  925. return 0;
  926. }
  927. try {
  928. int oldMaxInactiveInterval = session.getMaxInactiveInterval();
  929. session.setMaxInactiveInterval(maxInactiveInterval);
  930. return oldMaxInactiveInterval;
  931. } catch (IllegalStateException ise) {
  932. if (debug >= 1) {
  933. log("Can't set MaxInactiveInterval '" + maxInactiveInterval + "' for invalidated session id " + sessionId);
  934. }
  935. return 0;
  936. }
  937. }
  938. protected Comparator<Session> getComparator(String sortBy) {
  939. Comparator<Session> comparator = null;
  940. if ("CreationTime".equalsIgnoreCase(sortBy)) {
  941. comparator = new BaseSessionComparator<Date>() {
  942. @Override
  943. public Comparable<Date> getComparableObject(Session session) {
  944. return new Date(session.getCreationTime());
  945. }
  946. };
  947. } else if ("id".equalsIgnoreCase(sortBy)) {
  948. comparator = new BaseSessionComparator<String>() {
  949. @Override
  950. public Comparable<String> getComparableObject(Session session) {
  951. return session.getId();
  952. }
  953. };
  954. } else if ("LastAccessedTime".equalsIgnoreCase(sortBy)) {
  955. comparator = new BaseSessionComparator<Date>() {
  956. @Override
  957. public Comparable<Date> getComparableObject(Session session) {
  958. return new Date(session.getLastAccessedTime());
  959. }
  960. };
  961. } else if ("MaxInactiveInterval".equalsIgnoreCase(sortBy)) {
  962. comparator = new BaseSessionComparator<Date>() {
  963. @Override
  964. public Comparable<Date> getComparableObject(Session session) {
  965. return new Date(session.getMaxInactiveInterval());
  966. }
  967. };
  968. } else if ("new".equalsIgnoreCase(sortBy)) {
  969. comparator = new BaseSessionComparator<Boolean>() {
  970. @Override
  971. public Comparable<Boolean> getComparableObject(Session session) {
  972. return Boolean.valueOf(session.getSession().isNew());
  973. }
  974. };
  975. } else if ("locale".equalsIgnoreCase(sortBy)) {
  976. comparator = new BaseSessionComparator<String>() {
  977. @Override
  978. public Comparable<String> getComparableObject(Session session) {
  979. return JspHelper.guessDisplayLocaleFromSession(session);
  980. }
  981. };
  982. } else if ("user".equalsIgnoreCase(sortBy)) {
  983. comparator = new BaseSessionComparator<String>() {
  984. @Override
  985. public Comparable<String> getComparableObject(Session session) {
  986. return JspHelper.guessDisplayUserFromSession(session);
  987. }
  988. };
  989. } else if ("UsedTime".equalsIgnoreCase(sortBy)) {
  990. comparator = new BaseSessionComparator<Date>() {
  991. @Override
  992. public Comparable<Date> getComparableObject(Session session) {
  993. return new Date(SessionUtils.getUsedTimeForSession(session));
  994. }
  995. };
  996. } else if ("InactiveTime".equalsIgnoreCase(sortBy)) {
  997. comparator = new BaseSessionComparator<Date>() {
  998. @Override
  999. public Comparable<Date> getComparableObject(Session session) {
  1000. return new Date(SessionUtils.getInactiveTimeForSession(session));
  1001. }
  1002. };
  1003. } else if ("TTL".equalsIgnoreCase(sortBy)) {
  1004. comparator = new BaseSessionComparator<Date>() {
  1005. @Override
  1006. public Comparable<Date> getComparableObject(Session session) {
  1007. return new Date(SessionUtils.getTTLForSession(session));
  1008. }
  1009. };
  1010. }
  1011. //TODO: complete this to TTL, etc.
  1012. return comparator;
  1013. }
  1014. // ------------------------------------------------------ Private Constants
  1015. // These HTML sections are broken in relatively small sections, because of
  1016. // limited number of substitutions MessageFormat can process
  1017. // (maximum of 10).
  1018. private static final String APPS_HEADER_SECTION =
  1019. "<table border=\"1\" cellspacing=\"0\" cellpadding=\"3\">\n" +
  1020. "<tr>\n" +
  1021. " <td colspan=\"5\" class=\"title\">{0}</td>\n" +
  1022. "</tr>\n" +
  1023. "<tr>\n" +
  1024. " <td class=\"header-left\"><small>{1}</small></td>\n" +
  1025. " <td class=\"header-left\"><small>{2}</small></td>\n" +
  1026. " <td class=\"header-center\"><small>{3}</small></td>\n" +
  1027. " <td class=\"header-center\"><small>{4}</small></td>\n" +
  1028. " <td class=\"header-left\"><small>{5}</small></td>\n" +
  1029. "</tr>\n";
  1030. private static final String APPS_ROW_DETAILS_SECTION =
  1031. "<tr>\n" +
  1032. " <td class=\"row-left\" bgcolor=\"{6}\" rowspan=\"2\"><small><a href=\"{0}\">{1}</a>" +
  1033. "</small></td>\n" +
  1034. " <td class=\"row-left\" bgcolor=\"{6}\" rowspan=\"2\"><small>{2}</small></td>\n" +
  1035. " <td class=\"row-center\" bgcolor=\"{6}\" rowspan=\"2\"><small>{3}</small></td>\n" +
  1036. " <td class=\"row-center\" bgcolor=\"{6}\" rowspan=\"2\">" +
  1037. "<small><a href=\"{4}\">{5}</a></small></td>\n";
  1038. private static final String MANAGER_APP_ROW_BUTTON_SECTION =
  1039. " <td class=\"row-left\" bgcolor=\"{13}\">\n" +
  1040. " <small>\n" +
  1041. " &nbsp;{1}&nbsp;\n" +
  1042. " &nbsp;{3}&nbsp;\n" +
  1043. " &nbsp;{5}&nbsp;\n" +
  1044. " &nbsp;{7}&nbsp;\n" +
  1045. " </small>\n" +
  1046. " </td>\n" +
  1047. "</tr><tr>\n" +
  1048. " <td class=\"row-left\" bgcolor=\"{13}\">\n" +
  1049. " <form method=\"POST\" action=\"{8}\">\n" +
  1050. " <small>\n" +
  1051. " &nbsp;<input type=\"submit\" value=\"{9}\">&nbsp;{10}&nbsp;<input type=\"text\" name=\"idle\" size=\"5\" value=\"{11}\">&nbsp;{12}&nbsp;\n" +
  1052. " </small>\n" +
  1053. " </form>\n" +
  1054. " </td>\n" +
  1055. "</tr>\n";
  1056. private static final String STARTED_DEPLOYED_APPS_ROW_BUTTON_SECTION =
  1057. " <td class=\"row-left\" bgcolor=\"{13}\">\n" +
  1058. " &nbsp;<small>{1}</small>&nbsp;\n" +
  1059. " <form class=\"inline\" method=\"POST\" action=\"{2}\">" +
  1060. " <small><input type=\"submit\" value=\"{3}\"></small>" +
  1061. " </form>\n" +
  1062. " <form class=\"inline\" method=\"POST\" action=\"{4}\">" +
  1063. " <small><input type=\"submit\" value=\"{5}\"></small>" +
  1064. " </form>\n" +
  1065. " <form class=\"inline\" method=\"POST\" action=\"{6}\">" +
  1066. " <small><input type=\"submit\" value=\"{7}\"></small>" +
  1067. " </form>\n" +
  1068. " </td>\n" +
  1069. " </tr><tr>\n" +
  1070. " <td class=\"row-left\" bgcolor=\"{13}\">\n" +
  1071. " <form method=\"POST\" action=\"{8}\">\n" +
  1072. " <small>\n" +
  1073. " &nbsp;<input type=\"submit\" value=\"{9}\">&nbsp;{10}&nbsp;<input type=\"text\" name=\"idle\" size=\"5\" value=\"{11}\">&nbsp;{12}&nbsp;\n" +
  1074. " </small>\n" +
  1075. " </form>\n" +
  1076. " </td>\n" +
  1077. "</tr>\n";
  1078. private static final String STOPPED_DEPLOYED_APPS_ROW_BUTTON_SECTION =
  1079. " <td class=\"row-left\" bgcolor=\"{13}\" rowspan=\"2\">\n" +
  1080. " <form class=\"inline\" method=\"POST\" action=\"{0}\">" +
  1081. " <small><input type=\"submit\" value=\"{1}\"></small>" +
  1082. " </form>\n" +
  1083. " &nbsp;<small>{3}</small>&nbsp;\n" +
  1084. " &nbsp;<small>{5}</small>&nbsp;\n" +
  1085. " <form class=\"inline\" method=\"POST\" action=\"{6}\">" +
  1086. " <small><input type=\"submit\" value=\"{7}\"></small>" +
  1087. " </form>\n" +
  1088. " </td>\n" +
  1089. "</tr>\n<tr></tr>\n";
  1090. private static final String STARTED_NONDEPLOYED_APPS_ROW_BUTTON_SECTION =
  1091. " <td class=\"row-left\" bgcolor=\"{13}\" rowspan=\"2\">\n" +
  1092. " &nbsp;<small>{1}</small>&nbsp;\n" +
  1093. " <form class=\"inline\" method=\"POST\" action=\"{2}\">" +
  1094. " <small><input type=\"submit\" value=\"{3}\"></small>" +
  1095. " </form>\n" +
  1096. " <form class=\"inline\" method=\"POST\" action=\"{4}\">" +
  1097. " <small><input type=\"submit\" value=\"{5}\"></small>" +
  1098. " </form>\n" +
  1099. " &nbsp;<small>{7}</small>&nbsp;\n" +
  1100. " </td>\n" +
  1101. "</tr>\n<tr></tr>\n";
  1102. private static final String STOPPED_NONDEPLOYED_APPS_ROW_BUTTON_SECTION =
  1103. " <td class=\"row-left\" bgcolor=\"{13}\" rowspan=\"2\">\n" +
  1104. " <form class=\"inline\" method=\"POST\" action=\"{0}\">" +
  1105. " <small><input type=\"submit\" value=\"{1}\"></small>" +
  1106. " </form>\n" +
  1107. " &nbsp;<small>{3}</small>&nbsp;\n" +
  1108. " &nbsp;<small>{5}</small>&nbsp;\n" +
  1109. " &nbsp;<small>{7}</small>&nbsp;\n" +
  1110. " </td>\n" +
  1111. "</tr>\n<tr></tr>\n";
  1112. private static final String DEPLOY_SECTION =
  1113. "</table>\n" +
  1114. "<br>\n" +
  1115. "<table border=\"1\" cellspacing=\"0\" cellpadding=\"3\">\n" +
  1116. "<tr>\n" +
  1117. " <td colspan=\"2\" class=\"title\">{0}</td>\n" +
  1118. "</tr>\n" +
  1119. "<tr>\n" +
  1120. " <td colspan=\"2\" class=\"header-left\"><small>{1}</small></td>\n" +
  1121. "</tr>\n" +
  1122. "<tr>\n" +
  1123. " <td colspan=\"2\">\n" +
  1124. "<form method=\"post\" action=\"{2}\">\n" +
  1125. "<table cellspacing=\"0\" cellpadding=\"3\">\n" +
  1126. "<tr>\n" +
  1127. " <td class=\"row-right\">\n" +
  1128. " <small>{3}</s