/** * This software is licensed to you under the Apache License, Version 2.0 (the * "Apache License"). * * LinkedIn's contributions are made under the Apache License. If you contribute * to the Software, the contributions will be deemed to have been made under the * Apache License, unless you expressly indicate otherwise. Please do not make any * contributions that would be inconsistent with the Apache License. * * You may obtain a copy of the Apache License at http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, this software * distributed under the Apache License is distributed on an "AS IS" BASIS, WITHOUT * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the Apache * License for the specific language governing permissions and limitations for the * software governed under the Apache License. * * © 2012 LinkedIn Corp. All Rights Reserved. */ package com.senseidb.servlet; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.net.URL; import java.net.URLConnection; import java.net.URLDecoder; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import javax.servlet.ServletConfig; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.antlr.runtime.RecognitionException; import org.apache.commons.configuration.DataConfiguration; import org.apache.commons.configuration.MapConfiguration; import org.apache.http.client.utils.URLEncodedUtils; import org.apache.log4j.Logger; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import com.browseengine.bobo.api.BrowseSelection; import com.linkedin.norbert.javacompat.cluster.ClusterClient; import com.linkedin.norbert.javacompat.network.NetworkClientConfig; import com.senseidb.bql.parsers.BQLCompiler; import com.senseidb.cluster.client.SenseiNetworkClient; import com.senseidb.conf.SenseiConfParams; import com.senseidb.conf.SenseiFacetHandlerBuilder; import com.senseidb.search.node.Broker; import com.senseidb.search.node.SenseiBroker; import com.senseidb.search.node.SenseiSysBroker; import com.senseidb.search.node.broker.BrokerConfig; import com.senseidb.search.node.broker.LayeredBroker; import com.senseidb.search.req.ErrorType; import com.senseidb.search.req.SenseiError; import com.senseidb.search.req.SenseiHit; import com.senseidb.search.req.SenseiJSONQuery; import com.senseidb.search.req.SenseiRequest; import com.senseidb.search.req.SenseiResult; import com.senseidb.search.req.SenseiSystemInfo; import com.senseidb.svc.api.SenseiException; import com.senseidb.svc.impl.HttpRestSenseiServiceImpl; import com.senseidb.util.JsonTemplateProcessor; import com.senseidb.util.RequestConverter2; import com.senseidb.util.JSONUtil.FastJSONArray; import com.senseidb.util.JSONUtil.FastJSONObject; import com.yammer.metrics.Metrics; import com.yammer.metrics.core.Counter; import com.yammer.metrics.core.MetricName; public abstract class AbstractSenseiClientServlet extends ZookeeperConfigurableServlet { public static final int JSON_PARSING_ERROR = 489; public static final int BQL_EXTRA_FILTER_ERROR = 498; public static final int BQL_PARSING_ERROR = 499; public static final String BQL_STMT = "bql"; public static final String BQL_EXTRA_FILTER = "bql_extra_filter"; public static final String TOTAL_DOCS = "totaldocs"; private static final long serialVersionUID = 1L; private static final Logger logger = Logger.getLogger(AbstractSenseiClientServlet.class); private static final Logger queryLogger = Logger.getLogger("com.sensei.querylog"); private static final Counter totalDocsCounter = Metrics.newCounter(new MetricName(AbstractSenseiClientServlet.class, TOTAL_DOCS)); private ClusterClient _clusterClient = null; private SenseiNetworkClient _networkClient = null; private SenseiBroker _senseiBroker = null; private SenseiSysBroker _senseiSysBroker = null; private Map<String, String[]> _facetInfoMap = new HashMap<String, String[]>(); private BQLCompiler _compiler = null; private LayeredBroker federatedBroker; private JsonTemplateProcessor jsonTemplateProcessor = new JsonTemplateProcessor(); private Timer _statTimer; public AbstractSenseiClientServlet() { _statTimer = new Timer(true); } @Override public void init(ServletConfig config) throws ServletException { super.init(config); BrokerConfig brokerConfig = new BrokerConfig(senseiConf, loadBalancerFactory, serializer, pluginRegistry); brokerConfig.init(); _senseiBroker = brokerConfig.buildSenseiBroker(); _senseiSysBroker = brokerConfig.buildSysSenseiBroker(versionComparator); _networkClient = brokerConfig.getNetworkClient(); _clusterClient = brokerConfig.getClusterClient(); federatedBroker = pluginRegistry.getBeanByFullPrefix(SenseiConfParams.SENSEI_FEDERATED_BROKER, LayeredBroker.class); if (federatedBroker != null) { federatedBroker.warmUp(); } logger.info("Connecting to cluster: " + brokerConfig.getClusterName() +" ..."); _clusterClient.awaitConnectionUninterruptibly(); SenseiBrokerExport export = (SenseiBrokerExport)config.getServletContext().getAttribute("sensei.broker.export"); export.broker = _senseiBroker; export.sysBroker = _senseiSysBroker; export.networkClient = _networkClient; export.clusterClient = _clusterClient; export.servlet = this; int count = 0; while (true) { try { count++; logger.info("Trying to get sysinfo"); SenseiSystemInfo sysInfo = _senseiSysBroker.browse(new SenseiRequest()); _facetInfoMap = sysInfo != null && sysInfo.getFacetInfos() != null ? extractFacetInfo(sysInfo) : new HashMap<String, String[]>(); _compiler = new BQLCompiler(_facetInfoMap); break; } catch (Exception e) { logger.info("Hit exception trying to get sysinfo", e); if (count > 10) { logger.error("Give up after 10 tries to get sysinfo"); throw new ServletException(e.getMessage(), e); } else { try { Thread.sleep(2000); } catch (InterruptedException e2) { logger.error("Hit InterruptedException in getting sysinfo: ", e); } } } } // Start the stat timer to get some of the sys stat: _statTimer.scheduleAtFixedRate(new TimerTask() { public void run() { int totalDocs = 0; try { SenseiRequest req = new SenseiRequest(); req.setQuery(new SenseiJSONQuery(new FastJSONObject().put("query", "dummy:dummy"))); SenseiResult res = _senseiBroker.browse(req); totalDocs = res.getTotalDocs(); } catch(Exception e) { logger.warn("Error getting result", e); } if (totalDocs > 0) { totalDocsCounter.clear(); totalDocsCounter.inc(totalDocs); } else { logger.warn("Unable to get total docs"); } try { SenseiSystemInfo sysInfo = _senseiSysBroker.browse(new SenseiRequest()); if (sysInfo != null && sysInfo.getFacetInfos() != null) { _facetInfoMap = extractFacetInfo(sysInfo); _compiler.setFacetInfoMap(_facetInfoMap); } } catch (Exception e) { logger.info("Hit exception trying to get sysinfo", e); } } }, 60000, 60000); // Every minute. export.facetInfo = _facetInfoMap; logger.info("Cluster: "+ brokerConfig.getClusterName() +" successfully connected "); } public static Map<String, String[]> extractFacetInfo(SenseiSystemInfo sysInfo) { Map<String, String[]> facetInfoMap = new HashMap<String, String[]>(); Iterator<SenseiSystemInfo.SenseiFacetInfo> itr = sysInfo.getFacetInfos().iterator(); while (itr.hasNext()) { SenseiSystemInfo.SenseiFacetInfo facetInfo = itr.next(); Map<String, String> props = facetInfo.getProps(); facetInfoMap.put(facetInfo.getName(), new String[]{props.get("type"), props.get("column_type")}); } return facetInfoMap; } protected abstract SenseiRequest buildSenseiRequest(HttpServletRequest req) throws Exception; public static Map<String, String> getParameters(String query) throws Exception { Map<String, String> params = new HashMap<String, String>(); for (String param : query.split("&")) { String pair[] = param.split("="); String key = URLDecoder.decode(pair[0], "UTF-8"); String value = ""; if (pair.length > 1) { value = URLDecoder.decode(pair[1], "UTF-8"); } params.put(key, value); } return params; } private static class RequestContext { String query; JSONObject jsonObj; public String bqlStmt; public JSONObject templatesJson; public JSONObject compiledJson; public String content; public SenseiRequest senseiReq; } private void handleSenseiRequest(HttpServletRequest req, HttpServletResponse resp, Broker<SenseiRequest, SenseiResult> broker) throws ServletException, IOException { long time = System.currentTimeMillis(); int numHits = 0, totalDocs = 0; RequestContext requestContext = null; try { if ("post".equalsIgnoreCase(req.getMethod())) { requestContext = initializeRequestContextBasedOnPostParams(req, resp); } else { requestContext = initContextBasedOnGetParams(req, resp); } if (requestContext == null) { //the error has been already logged return; } if (requestContext.jsonObj != null) { requestContext.bqlStmt = requestContext.jsonObj.optString(BQL_STMT); requestContext.templatesJson = requestContext.jsonObj.optJSONObject(JsonTemplateProcessor.TEMPLATE_MAPPING_PARAM); requestContext.compiledJson = null; if (requestContext.bqlStmt.length() > 0) { boolean successfull = handleBqlRequest(req, resp, requestContext); if (!successfull) { return; } } else { // This is NOT a BQL statement requestContext.query = "json=" + requestContext.content; requestContext.compiledJson = requestContext.jsonObj; } if (requestContext.templatesJson != null) { requestContext.compiledJson.put(JsonTemplateProcessor.TEMPLATE_MAPPING_PARAM, requestContext.templatesJson); } requestContext.senseiReq = SenseiRequest.fromJSON(requestContext.compiledJson, _facetInfoMap); } SenseiResult res = broker.browse(requestContext.senseiReq); numHits = res.getNumHits(); totalDocs = res.getTotalDocs(); sendResponse(req, resp, requestContext.senseiReq, res); } catch (JSONException e) { try { writeEmptyResponse(req, resp, new SenseiError(e.getMessage(), ErrorType.JsonParsingError)); } catch (Exception ex) { throw new ServletException(e); } } catch (Exception e) { try { logger.error(e.getMessage(), e); if (e.getCause() != null && e.getCause() instanceof JSONException) { writeEmptyResponse(req, resp, new SenseiError(e.getMessage(), ErrorType.JsonParsingError)); } else { writeEmptyResponse(req, resp, new SenseiError(e.getMessage(), ErrorType.InternalError)); } } catch (Exception ex) { throw new ServletException(e); } } finally { if (queryLogger.isDebugEnabled() && requestContext != null && requestContext.query != null) { queryLogger.debug(String.format("hits(%d/%d) took %dms: %s", numHits, totalDocs, System.currentTimeMillis() - time, requestContext.query)); } } } public RequestContext initContextBasedOnGetParams(HttpServletRequest req, HttpServletResponse resp) throws Exception, SenseiException, UnsupportedEncodingException { RequestContext requestContext; requestContext = new RequestContext(); requestContext.content = req.getParameter("json"); if (requestContext.content != null) { if (requestContext.content.length() == 0) requestContext.content = "{}"; try { requestContext.jsonObj = new FastJSONObject(requestContext.content); } catch(JSONException jse) { logger.error("JSON parsing error", jse); writeEmptyResponse(req, resp, new SenseiError(jse.getMessage(), ErrorType.JsonParsingError)); return null; } } else { requestContext.senseiReq = buildSenseiRequest(req); requestContext.query = URLEncodedUtils.format( HttpRestSenseiServiceImpl.convertRequestToQueryParams(requestContext.senseiReq), "UTF-8"); } return requestContext; } public RequestContext initializeRequestContextBasedOnPostParams(HttpServletRequest req, HttpServletResponse resp) throws IOException, Exception { RequestContext requestContext; requestContext = new RequestContext(); BufferedReader reader = req.getReader(); requestContext.content = readContent(reader); if (requestContext.content == null || requestContext.content.length() == 0) requestContext.content = "{}"; try { requestContext.jsonObj = new FastJSONObject(requestContext.content); } catch(JSONException jse) { String contentType = req.getHeader("Content-Type"); if (contentType != null && contentType.indexOf("json") >= 0) { logger.error("JSON parsing error", jse); writeEmptyResponse(req, resp, new SenseiError(jse.getMessage(), ErrorType.JsonParsingError)); return null; } logger.warn("Old client or json error", jse); // Fall back to the old REST API. In the future, we should // consider reporting JSON exceptions here. requestContext.senseiReq = DefaultSenseiJSONServlet.convertSenseiRequest( new DataConfiguration(new MapConfiguration(getParameters(requestContext.content)))); requestContext.query = requestContext.content; } return requestContext; } public boolean handleBqlRequest(HttpServletRequest req, HttpServletResponse resp, RequestContext requestContext) throws Exception, JSONException { try { if (requestContext.jsonObj.length() == 1) requestContext.query = "bql=" + requestContext.bqlStmt; else requestContext.query = "json=" + requestContext.content; // Disable variables replacing before bql compling, since that data representation in json and bql is quite different for now. //requestContext.bqlStmt = (String) jsonTemplateProcessor.process(requestContext.bqlStmt, jsonTemplateProcessor.getTemplates(requestContext.jsonObj)); requestContext.compiledJson = _compiler.compile(requestContext.bqlStmt); } catch (RecognitionException e) { String errMsg = _compiler.getErrorMessage(e); if (errMsg == null) { errMsg = "Unknown parsing error."; } logger.error("BQL parsing error: " + errMsg + ", BQL: " + requestContext.bqlStmt); writeEmptyResponse(req, resp, new SenseiError(errMsg, ErrorType.BQLParsingError)); return false; } // Handle extra BQL filter if it exists String extraFilter = requestContext.jsonObj.optString(BQL_EXTRA_FILTER); JSONObject predObj = null; if (extraFilter.length() > 0) { String bql2 = "SELECT * WHERE " + extraFilter; try { predObj = _compiler.compile(bql2); } catch (RecognitionException e) { String errMsg = _compiler.getErrorMessage(e); if (errMsg == null) { errMsg = "Unknown parsing error."; } logger.error("BQL parsing error for additional preds: " + errMsg + ", BQL: " + bql2); writeEmptyResponse(req, resp, new SenseiError("BQL parsing error for additional preds: " + errMsg + ", BQL: " + bql2, ErrorType.BQLParsingError)); return false; } // Combine filters JSONArray filter_list = new FastJSONArray(); JSONObject currentFilter = requestContext.compiledJson.optJSONObject("filter"); if (currentFilter != null) { filter_list.put(currentFilter); } JSONArray selections = predObj.optJSONArray("selections"); if (selections != null) { for (int i = 0; i < selections.length(); ++i) { JSONObject pred = selections.getJSONObject(i); if (pred != null) { filter_list.put(pred); } } } JSONObject additionalFilter = predObj.optJSONObject("filter"); if (additionalFilter != null) { filter_list.put(additionalFilter); } if (filter_list.length() > 1) { requestContext.compiledJson.put("filter", new FastJSONObject().put("and", filter_list)); } else if (filter_list.length() == 1) { requestContext.compiledJson.put("filter", filter_list.get(0)); } } JSONObject metaData = requestContext.compiledJson.optJSONObject("meta"); if (metaData != null) { JSONArray variables = metaData.optJSONArray("variables"); if (variables != null) { for (int i = 0; i < variables.length(); ++i) { String var = variables.getString(i); if (requestContext.templatesJson == null || requestContext.templatesJson.opt(var) == null) { writeEmptyResponse(req, resp, new SenseiError("[line:0, col:0] Variable " + var + " is not found.", ErrorType.BQLParsingError)); return false; } } } } return true; } private void writeEmptyResponse(HttpServletRequest req, HttpServletResponse resp, SenseiError senseiError) throws Exception { SenseiResult res = new SenseiResult(); res.addError(senseiError); sendResponse(req, resp, new SenseiRequest(), res); } private void sendResponse(HttpServletRequest req, HttpServletResponse resp, SenseiRequest senseiReq, SenseiResult res) throws Exception { OutputStream ostream = resp.getOutputStream(); convertResult(req, senseiReq, res, ostream); ostream.flush(); } private void handleStoreGetRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { long time = System.currentTimeMillis(); int numHits = 0, totalDocs = 0; String query = null; SenseiRequest senseiReq = null; try { JSONArray ids = null; if ("post".equalsIgnoreCase(req.getMethod())) { BufferedReader reader = req.getReader(); ids = new FastJSONArray(readContent(reader)); } else { String jsonString = req.getParameter("json"); if (jsonString != null) ids = new FastJSONArray(jsonString); } query = "get=" + String.valueOf(ids); String[] vals = RequestConverter2.getStrings(ids); if (vals != null && vals.length != 0) { senseiReq = new SenseiRequest(); senseiReq.setFetchStoredValue(true); senseiReq.setCount(vals.length); BrowseSelection sel = new BrowseSelection(SenseiFacetHandlerBuilder.UID_FACET_NAME); sel.setValues(vals); senseiReq.addSelection(sel); } SenseiResult res = null; if (senseiReq != null) res =_senseiBroker.browse(senseiReq); if (res != null) { numHits = res.getNumHits(); totalDocs = res.getTotalDocs(); } JSONObject ret = new FastJSONObject(); JSONObject obj = null; if (res != null && res.getSenseiHits() != null) { for (SenseiHit hit : res.getSenseiHits()) { try { obj = new FastJSONObject(hit.getSrcData()); ret.put(String.valueOf(hit.getUID()), obj); } catch(Exception ex) { logger.warn(ex.getMessage(), ex); } } } OutputStream ostream = resp.getOutputStream(); ostream.write(ret.toString().getBytes("UTF-8")); ostream.flush(); } catch (Exception e) { throw new ServletException(e.getMessage(),e); } finally { if (queryLogger.isDebugEnabled() && query != null) { queryLogger.debug(String.format("hits(%d/%d) took %dms: %s", numHits, totalDocs, System.currentTimeMillis() - time, query)); } } } private void handleSystemInfoRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { try { SenseiSystemInfo res = _senseiSysBroker.browse(new SenseiRequest()); OutputStream ostream = resp.getOutputStream(); convertResult(req, res, ostream); ostream.flush(); } catch (Exception e) { throw new ServletException(e.getMessage(),e); } } private void handleJMXRequest(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { InputStream is = null; OutputStream os = null; try { String myPath = req.getRequestURI().substring(req.getServletPath().length()+11); URL adminUrl = null; if (myPath.indexOf('/') > 0) { adminUrl = new URL(new StringBuilder(URLDecoder.decode(myPath.substring(0, myPath.indexOf('/')), "UTF-8")) .append("/admin/jmx") .append(myPath.substring(myPath.indexOf('/'))).toString()); } else { adminUrl = new URL(new StringBuilder(URLDecoder.decode(myPath, "UTF-8")) .append("/admin/jmx").toString()); } URLConnection conn = adminUrl.openConnection(); byte[] buffer = new byte[8192]; // 8k int len = 0; InputStream ris = req.getInputStream(); while((len=ris.read(buffer)) > 0) { if (!conn.getDoOutput()) { conn.setDoOutput(true); os = conn.getOutputStream(); } os.write(buffer, 0, len); } if (os != null) os.flush(); is = conn.getInputStream(); OutputStream ros = resp.getOutputStream(); while((len=is.read(buffer)) > 0) { ros.write(buffer, 0, len); } ros.flush(); } catch (Exception e) { throw new ServletException(e.getMessage(),e); } finally { if (is != null) is.close(); if (os != null) os.close(); } } private static String readContent(BufferedReader reader) throws IOException{ StringBuilder jb = new StringBuilder(); String line = null; while ((line = reader.readLine()) != null) jb.append(line); return jb.toString(); } @Override protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { if (req.getCharacterEncoding() == null) req.setCharacterEncoding("UTF-8"); resp.setContentType("application/json; charset=utf-8"); resp.setCharacterEncoding("UTF-8"); resp.setHeader("Access-Control-Allow-Origin", "*"); resp.setHeader("Access-Control-Allow-Methods", "GET, POST"); resp.setHeader("Access-Control-Allow-Headers", "Origin, Content-Type, X-Requested-With, Accept"); if (null == req.getPathInfo() || "/".equalsIgnoreCase(req.getPathInfo())) { handleSenseiRequest(req, resp, _senseiBroker); } else if ("/get".equalsIgnoreCase(req.getPathInfo())) { handleStoreGetRequest(req, resp); } else if ("/sysinfo".equalsIgnoreCase(req.getPathInfo())) { handleSystemInfoRequest(req, resp); } else if (req.getPathInfo().startsWith("/admin/jmx/")) { handleJMXRequest(req, resp); }else if (req.getPathInfo().startsWith("/federatedBroker/")) { if (federatedBroker == null) { try { writeEmptyResponse(req, resp, new SenseiError("The federated broker wasn't initialized", ErrorType.FederatedBrokerUnavailable)) ; } catch (Exception e) { throw new RuntimeException(e); } } handleSenseiRequest(req, resp, federatedBroker); } else { handleSenseiRequest(req, resp, _senseiBroker); } } @Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { doGet(req, resp); } @Override protected void doOptions(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { resp.setHeader("Access-Control-Allow-Origin", "*"); resp.setHeader("Access-Control-Allow-Methods", "GET, POST"); resp.setHeader("Access-Control-Allow-Headers", "Origin, Content-Type, X-Requested-With, Accept"); } protected abstract void convertResult(HttpServletRequest httpReq, SenseiSystemInfo info, OutputStream ostream) throws Exception; protected abstract void convertResult(HttpServletRequest httpReq, SenseiRequest req,SenseiResult res,OutputStream ostream) throws Exception; @Override public void destroy() { try{ try{ if (_senseiBroker!=null){ _senseiBroker.shutdown(); _senseiBroker = null; } } finally{ try { if (_senseiSysBroker!=null){ _senseiSysBroker.shutdown(); _senseiSysBroker = null; } } finally { try{ if (_networkClient!=null){ _networkClient.shutdown(); _networkClient = null; } } finally{ try { if (_clusterClient!=null) { _clusterClient.shutdown(); _clusterClient = null; } } finally { _statTimer.cancel(); } } } } } finally{ super.destroy(); } } /** * This class is a hack to allow the servlet to export the broker components * to outside services. Since this components are instantiated here as part * of the servlet initialization, it is a workaround to expose it through a * placed holder instance injected via ServletContext by the builder of this * servlet and its container. */ public static class SenseiBrokerExport { public AbstractSenseiClientServlet servlet; public ClusterClient clusterClient; public SenseiNetworkClient networkClient; public SenseiSysBroker sysBroker; public SenseiBroker broker; public Map<String, String[]> facetInfo; } }