View Javadoc
1   /*
2    * Licensed under the GPL License. You may not use this file except in compliance with the License.
3    * You may obtain a copy of the License at
4    *
5    *   https://www.gnu.org/licenses/old-licenses/gpl-2.0.html
6    *
7    * THIS PACKAGE IS PROVIDED "AS IS" AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING,
8    * WITHOUT LIMITATION, THE IMPLIED WARRANTIES OF MERCHANTIBILITY AND FITNESS FOR A PARTICULAR
9    * PURPOSE.
10   */
11  package psiprobe.controllers.sql;
12  
13  import jakarta.servlet.http.HttpServletRequest;
14  import jakarta.servlet.http.HttpServletResponse;
15  import jakarta.servlet.http.HttpSession;
16  
17  import java.sql.Connection;
18  import java.sql.PreparedStatement;
19  import java.sql.ResultSet;
20  import java.sql.ResultSetMetaData;
21  import java.sql.SQLException;
22  import java.util.ArrayList;
23  import java.util.LinkedHashMap;
24  import java.util.List;
25  import java.util.Map;
26  
27  import javax.naming.NamingException;
28  import javax.sql.DataSource;
29  
30  import org.apache.catalina.Context;
31  import org.slf4j.Logger;
32  import org.slf4j.LoggerFactory;
33  import org.springframework.beans.factory.annotation.Value;
34  import org.springframework.stereotype.Controller;
35  import org.springframework.web.bind.ServletRequestUtils;
36  import org.springframework.web.bind.annotation.RequestMapping;
37  import org.springframework.web.servlet.ModelAndView;
38  import org.springframework.web.util.HtmlUtils;
39  
40  import psiprobe.controllers.AbstractContextHandlerController;
41  import psiprobe.model.sql.DataSourceTestInfo;
42  
43  /**
44   * Executes an SQL query through a given datasource to test database connectivity. Displays results
45   * returned by the query.
46   */
47  @Controller
48  public class ExecuteSqlController extends AbstractContextHandlerController {
49  
50    /** The Constant logger. */
51    private static final Logger logger = LoggerFactory.getLogger(ExecuteSqlController.class);
52  
53    @RequestMapping(path = "/sql/recordset.ajax")
54    @Override
55    public ModelAndView handleRequest(HttpServletRequest request, HttpServletResponse response)
56        throws Exception {
57      return super.handleRequest(request, response);
58    }
59  
60    @Override
61    protected ModelAndView handleContext(String contextName, Context context,
62        HttpServletRequest request, HttpServletResponse response) throws Exception {
63  
64      String resourceName = ServletRequestUtils.getStringParameter(request, "resource");
65      String sql = ServletRequestUtils.getStringParameter(request, "sql");
66  
67      if (sql == null || sql.isEmpty() || sql.trim().isEmpty()) {
68        request.setAttribute("errorMessage",
69            getMessageSourceAccessor().getMessage("probe.src.dataSourceTest.sql.required"));
70  
71        return new ModelAndView(getViewName());
72      }
73  
74      int maxRows = ServletRequestUtils.getIntParameter(request, "maxRows", 0);
75      int rowsPerPage = ServletRequestUtils.getIntParameter(request, "rowsPerPage", 0);
76      int historySize = ServletRequestUtils.getIntParameter(request, "historySize", 0);
77  
78      // store current option values and query history in a session attribute
79  
80      HttpSession sess = request.getSession(false);
81      DataSourceTestInfo sessData =
82          (DataSourceTestInfo) sess.getAttribute(DataSourceTestInfo.DS_TEST_SESS_ATTR);
83  
84      synchronized (sess) {
85        if (sessData == null) {
86          sessData = new DataSourceTestInfo();
87          sess.setAttribute(DataSourceTestInfo.DS_TEST_SESS_ATTR, sessData);
88        }
89  
90        sessData.setMaxRows(maxRows);
91        sessData.setRowsPerPage(rowsPerPage);
92        sessData.setHistorySize(historySize);
93        sessData.addQueryToHistory(sql);
94      }
95  
96      DataSource dataSource = null;
97  
98      try {
99        dataSource = getContainerWrapper().getResourceResolver().lookupDataSource(context,
100           resourceName, getContainerWrapper());
101     } catch (NamingException e) {
102       request.setAttribute("errorMessage", getMessageSourceAccessor().getMessage(
103           "probe.src.dataSourceTest.resource.lookup.failure", new Object[] {resourceName}));
104       logger.trace("", e);
105     }
106 
107     if (dataSource == null) {
108       request.setAttribute("errorMessage", getMessageSourceAccessor().getMessage(
109           "probe.src.dataSourceTest.resource.lookup.failure", new Object[] {resourceName}));
110     } else {
111       List<Map<String, String>> results = null;
112       int rowsAffected = 0;
113 
114       try {
115         // TODO: use Spring's jdbc template?
116         try (Connection conn = dataSource.getConnection()) {
117           conn.setAutoCommit(true);
118 
119           try (PreparedStatement stmt = conn.prepareStatement(sql)) {
120             boolean hasResultSet = stmt.execute();
121 
122             if (!hasResultSet) {
123               rowsAffected = stmt.getUpdateCount();
124             } else {
125               results = new ArrayList<>();
126 
127               try (ResultSet rs = stmt.getResultSet()) {
128                 ResultSetMetaData metaData = rs.getMetaData();
129 
130                 while (rs.next() && (maxRows < 0 || results.size() < maxRows)) {
131                   Map<String, String> record = new LinkedHashMap<>();
132 
133                   for (int i = 1; i <= metaData.getColumnCount(); i++) {
134                     String value = rs.getString(i);
135 
136                     if (rs.wasNull()) {
137                       value = getMessageSourceAccessor()
138                           .getMessage("probe.src.dataSourceTest.sql.null");
139                     } else {
140                       value = HtmlUtils.htmlEscape(value);
141                     }
142 
143                     // Pad the keys of columns with existing labels so they are distinct
144                     StringBuilder key = new StringBuilder(metaData.getColumnLabel(i));
145                     while (record.containsKey(key.toString())) {
146                       key.append(" ");
147                     }
148                     record.put(HtmlUtils.htmlEscape(key.toString()), value);
149                   }
150 
151                   results.add(record);
152                 }
153               }
154 
155               rowsAffected = results.size();
156             }
157           }
158         }
159 
160         // store the query results in the session attribute in order
161         // to support a result set pagination feature without re-executing the query
162 
163         synchronized (sess) {
164           sessData.setResults(results);
165         }
166 
167         ModelAndView mv = new ModelAndView(getViewName(), "results", results);
168         mv.addObject("rowsAffected", String.valueOf(rowsAffected));
169         mv.addObject("rowsPerPage", String.valueOf(rowsPerPage));
170 
171         return mv;
172       } catch (SQLException e) {
173         String message = getMessageSourceAccessor()
174             .getMessage("probe.src.dataSourceTest.sql.failure", new Object[] {e.getMessage()});
175         logger.error(message, e);
176         request.setAttribute("errorMessage", message);
177       }
178     }
179 
180     return new ModelAndView(getViewName());
181   }
182 
183   @Override
184   protected boolean isContextOptional() {
185     return true;
186   }
187 
188   @Value("ajax/sql/recordset")
189   @Override
190   public void setViewName(String viewName) {
191     super.setViewName(viewName);
192   }
193 
194 }