package de.superx.saiku.cube; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import java.io.BufferedReader; import java.io.BufferedWriter; import java.io.IOException; import java.io.InputStreamReader; import java.io.OutputStreamWriter; import java.io.UnsupportedEncodingException; import java.net.CookieHandler; import java.net.CookieManager; import java.net.HttpCookie; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import org.apache.log4j.Logger; import org.junit.Before; import org.junit.Ignore; /** * Test your cube. You can execute a MDX-Query over the REST Service. * Execute MDX with executeMDX (it returns the resultMap). You can validate your result by using validateResultEquals function or directly work with the resultMap. * The result is a map with the values position in the table as key. (position: "column:row"; i.e: if you want the value in 2nd column and first row the key would be: "1:0") * To know which position your value has, take a look at the result table Saiku returns for your query. * * Also you can validate that a cube with a given name exists with validateCubeExists. * * Default login is linde/linde!. * If you want to use another login or url you could override connectInitAndAuth() and change the values before calling super.connectInitAndAuth() * * TODO: better result validation. Atm only functions for equals are given, if you need to do some other validation you need to work with the resultMap directly. * * @author rogat */ @Ignore public abstract class AbstractCubeTest { private static final Logger LOG = Logger.getLogger(AbstractCubeTest.class); protected String url = System.getProperty("url", "http://127.0.0.1:8080/"); protected String user = "linde"; protected String password = "linde!"; private final CookieManager cookieManager = new CookieManager(); private String qisName = null; private String xsrfToken; protected Map resultMap; protected List cubeList; /** * Initializes the the cookieManager, fetches the QisName authenticates with HISINONE and superX. * @throws IOException */ @Before public void connectInitAndAuth() throws IOException { CookieHandler.setDefault(cookieManager); //open connection to url and extract the name of the server fetchAndSetQisName(); //sign in HISINONE HttpURLConnection con = auth(user, password); assertTrue("HISINONE-Login war nicht erfolgereich.", !readContent(con).contains("infobox error_infobox messages-infobox")); //auth with superX authSuperx(); //fetch cubes fetchCubes(); } /** * Tries to find the cube name in the list fetched from server. * @param cubeName */ protected void validateCubeExists(String cubeName) { assertTrue("A cube with the name " + cubeName + " does not exists or is not visible for user " + user, cubeList.contains(cubeName)); } /** * Tries to find the value at the given position. If the value doesn't match -> test failed. * @param position * @param value */ protected void validateResultEquals(String position, Double value) { Double resultValue = resultMap.get(position); assertTrue("Value " + value + " not found at position " + position, resultValue != null && resultValue.equals(value)); } /** * Tries to find the value at the given position. If the value doesn't match -> test failed. * @param position * @param value */ protected void validateResultEquals(String position, double value) { validateResultEquals(position, Double.valueOf(value)); } /** * Tries to find the value at the given position. If the value doesn't match -> test failed. * @param position * @param value */ protected void validateResultEquals(String position, int value) { validateResultEquals(position, Double.valueOf(value)); } /** * Tries to find the value at the given position. If the value doesn't match -> test failed. * @param column * @param row * @param value */ protected void validateResultEquals(int column, int row, Double value) { validateResultEquals(column + ":" + row, value); } /** * Tries to find the value at the given position. If the value doesn't match -> test failed. * @param column * @param row * @param value */ protected void validateResultEquals(int column, int row, double value) { validateResultEquals(column + ":" + row, Double.valueOf(value)); } /** * Tries to find the value at the given position. If the value doesn't match -> test failed. * @param column * @param row * @param value */ protected void validateResultEquals(int column, int row, int value) { validateResultEquals(column + ":" + row, Double.valueOf(value)); } /** * Extracts the resultData from the json result. * The returned map contains the values at a position. Key is the position as a string like "0:4" (= Column 0, Row 4) * @param result * @return */ private Map convertResult(String result) { resultMap = new HashMap(); String tmp_result = result; //go through the result and extract every resultValue and its position int datacell_index = tmp_result.indexOf(STR_DATACELL); while ( datacell_index != -1) { int begin_index = tmp_result.lastIndexOf('{', datacell_index); int end_index = tmp_result.indexOf('}', datacell_index); String dataCell = tmp_result.substring(begin_index, end_index); //here we got a datacell (contains value, position and more) String value = extractValueFromDatacell(dataCell, STR_VALUE); //extract the value String position = extractValueFromDatacell(dataCell, STR_POSITION); //extract the position Double d_value = Double.valueOf(0); try { d_value = Double.valueOf(Double.parseDouble(value)); } catch (NumberFormatException e) { // this is likely to be an empty value -> treat it like 0 } resultMap.put(position, d_value); //store value at position in resultMap tmp_result = tmp_result.substring(end_index); //cut away the already processed datacell datacell_index = tmp_result.indexOf(STR_DATACELL); } return resultMap; } /** * Extracts the value from a jsonString. * (I.e: extractValueFromDatacell("\"value\":\"1\"", "\"value\":\"") -> returns "1") * @param text * @param identifier * @return */ private static String extractValueFromDatacell(String text, String identifier) { String str = text; int index = str.indexOf(identifier); str = str.substring(index + identifier.length()); String value = str.substring(0, str.indexOf("\"")); return value; } /** * Prepares a HttpURLConnection connection for the Saiku rest call to execute a query * @return prepared HttpURLConnection * @throws IOException */ private HttpURLConnection prepareOlapConnection() throws IOException { HttpURLConnection con = openConnection("superx/rest/saiku/api/query/execute"); con.setRequestProperty("X-Requested-With", "XMLHttpRequest"); con.setRequestProperty("X-XSRF-TOKEN", xsrfToken); con.setRequestMethod("POST"); con.setRequestProperty("Content-Type", "application/json"); con.setRequestProperty("Accept", "application/json, text/javascript, */*; q=0.01"); con.setDoOutput(true); con.setDoInput(true); return con; } /** * Builds and executes a OlapQuery with the given MDX * Converts and stores the result in this.resultMap * Also validated whether a cube with the given name exists * @param cubeName name of the cube (e.g. res_project_cube) * @param mdx * @return resultMap * @throws IOException */ protected Map executeMDX(String cubeName, String mdx) throws IOException { validateCubeExists(cubeName); HttpURLConnection con = prepareOlapConnection(); BufferedWriter bufferedWriter = new BufferedWriter(new OutputStreamWriter(con.getOutputStream())); bufferedWriter.write(String.format(QUERY_TEMPLATE, cubeName, mdx.replaceAll("\n", " "))); bufferedWriter.flush(); bufferedWriter.close(); return convertResult(readContent(con)); } /** * Authenticate with superX after the authentication with HISINONE is made * @throws IOException */ private void authSuperx() throws IOException { //start sso String token = extractToken(); LOG.debug("Token: " + token); assertNotNull("No SSO token found.", token); //finish sso with token HttpURLConnection con = openConnection("superx/servlet/SuperXmlAnmeldung?token=" + token); con.setRequestMethod("POST"); send(con); // find the xsrf cookie and store it (we need to add it to the header for saiku requests) for (HttpCookie cookie : cookieManager.getCookieStore().getCookies()) { if (cookie.getName().equals("XSRF-TOKEN")) { xsrfToken = cookie.getValue(); break; } } //validate sso con = openConnection("superx/saiku/"); assertTrue("Could not log into Saiku...SSO failed?", send(con) == 200); //start session con = openConnection("superx/rest/saiku/session?username=" + encodeURL(user) + "&password=notsoimportant"); con.setRequestProperty("X-Requested-With", "XMLHttpRequest"); con.setRequestProperty("X-XSRF-TOKEN", xsrfToken); con.setRequestMethod("POST"); send(con); } /** * Fetch all possible cubes from server and stores them in cubeList * @throws IOException */ private void fetchCubes() throws IOException { HttpURLConnection con = openConnection("superx/rest/saiku/" + encodeURL(user) + "/discover/refresh"); con.setRequestProperty("X-Requested-With", "XMLHttpRequest"); con.setRequestProperty("X-XSRF-TOKEN", xsrfToken); String content = readContent(con); //go through the content and extract the cubeNames cubeList = new ArrayList(); int cubeCell_index = content.indexOf(STR_CAPTION); while ( cubeCell_index != -1) { int begin_index = content.lastIndexOf('{', cubeCell_index); int end_index = content.indexOf('}', cubeCell_index); String cubeCell = content.substring(begin_index, end_index); //here we got a cubeCell (contains name and more) String name = extractValueFromDatacell(cubeCell, STR_NAME); //extract cubeName // String caption = extractValueFromDatacell(cubeCell, STR_CAPTION); cubeList.add(name); content = content.substring(end_index); //cut the already processed cubeCell away cubeCell_index = content.indexOf(STR_CAPTION); } } /** * Established the connection and reads the httpResponeCode * @param con * @return ResponseCode * @throws IOException */ private static int send(HttpURLConnection con) throws IOException { int status = con.getResponseCode(); LOG.debug("ResponseConde: " + status); assertTrue("HTTP-ResponseCode: " + status + ". For URL: " + con.getURL(), status == 200); return status; } /** * reads content of a HttpURLConnection * @param con * @return * @throws IOException */ private static String readContent(HttpURLConnection con) throws IOException { int status = con.getResponseCode(); LOG.debug("ResponseConde: " + status); assertTrue("HTTP-ResponseCode: " + status + ". For URL: " + con.getURL(), status == 200); BufferedReader in = null; if (status > 299) { in = new BufferedReader(new InputStreamReader(con.getErrorStream())); } else { in = new BufferedReader(new InputStreamReader(con.getInputStream())); } String inputLine; StringBuffer content = new StringBuffer(); while ((inputLine = in.readLine()) != null) { content.append(inputLine); } in.close(); LOG.debug("Content: " + content); return content.toString(); } /** * Calls http://......./rds?state=redirect&sso=superx and extracts the token to authenticated with superX * @param con * @return token * @throws IOException */ private String extractToken() throws IOException { //make the call HttpURLConnection con = openConnection(qisName + "rds?state=redirect&sso=superx"); String token = null; int status = con.getResponseCode(); if (status == 200) { // was successful? BufferedReader in = new BufferedReader(new InputStreamReader(con.getInputStream())); String inputLine; //search the content for the token while ((inputLine = in.readLine()) != null) { int index = inputLine.indexOf("value=\""); if (index != -1) { //extract the token from line token = inputLine.substring(index + 7, inputLine.indexOf("\">")); break; } } } return token; } /** * Authenticate with HISINONE * @param user * @param pass * @return HttpURLConnection * @throws IOException */ private HttpURLConnection auth(String user, String pass) throws IOException { HttpURLConnection con = openConnection(qisName + "rds?state=user&type=1&username=" + encodeURL(user) + "&password=" + encodeURL(pass)); return con; } /** * Extracts the qisName from the content send back from URL and stores it in var: qisName * (qisName has usually values like: "qisserver" or "hisinone") * @throws IOException */ private void fetchAndSetQisName() throws IOException { HttpURLConnection con = openConnection(""); String content = readContent(con); int startIndex = content.indexOf("0;URL=/"); int endIndex = content.indexOf("rds?state"); if (startIndex != -1 && endIndex != -1) { qisName = content.substring(startIndex + 7, endIndex); } assertNotNull("Could extract QIS_NAME", qisName); } /** * Prepares a connection on URL + urlSuffix * The connection will not be established * @param urlSuffix * @return * @throws IOException */ private HttpURLConnection openConnection(String urlSuffix) throws IOException { String tmp_url = this.url + urlSuffix; LOG.debug("\n\nopening: " + tmp_url.toString()); HttpURLConnection con = (HttpURLConnection) new URL(tmp_url).openConnection(); con.setConnectTimeout(5000); return con; } /** * UTF8 encoding * @param str * @return */ public static String encodeURL(String str) { if (str == null) { return ""; } try { return URLEncoder.encode(str, "UTF8"); } catch (final UnsupportedEncodingException e) { return str; } } private final String STR_NAME = "\"name\":\""; private final String STR_CAPTION = "\"caption\":\""; private final String STR_VALUE = "\"value\":\""; private final String STR_POSITION = "\"position\":\""; private final String STR_DATACELL = "\"type\":\"DATA_CELL\""; private final String QUERY_TEMPLATE = "{\n" + " \"queryModel\": {\n" + " \n" + " },\n" + " \"cube\": {\n" + " \"uniqueName\": \"[BI].[HISinOne].[HISinOne].[%s]\",\n" + " \"name\": \"res_employment_cube\",\n" + " \"connection\": \"BI\",\n" + " \"catalog\": \"HISinOne\",\n" + " \"schema\": \"HISinOne\",\n" + " \"caption\": null,\n" + " \"visible\": false\n" + " },\n" + " \"mdx\": \"%s\",\n" + " \"name\": \"7E6533B9-FB6E-8FAA-B8D9-C3737EFEA0E7\",\n" + " \"parameters\": {\n" + " \n" + " },\n" + " \"plugins\": {\n" + " \n" + " },\n" + " \"properties\": {\n" + " \"saiku.olap.query.automatic_execution\": true,\n" + " \"saiku.olap.query.nonempty\": true,\n" + " \"saiku.olap.query.nonempty.rows\": true,\n" + " \"saiku.olap.query.nonempty.columns\": true,\n" + " \"saiku.ui.render.mode\": \"table\",\n" + " \"saiku.olap.query.filter\": true,\n" + " \"saiku.olap.result.formatter\": \"flat\",\n" + " \"org.saiku.query.explain\": true,\n" + " \"saiku.olap.query.drillthrough\": true,\n" + " \"org.saiku.connection.scenario\": false\n" + " },\n" + " \"metadata\": {\n" + " \n" + " },\n" + " \"queryType\": \"OLAP\",\n" + " \"type\": \"MDX\"\n" + "}"; private final String EMPTY_RESULT = "{\"cellset\":null,\"rowTotalsLists\":null,\"colTotalsLists\":null,\"runtime\":null,\"error\":\"NullPointerException: \",\"height\":null,\"width\":null,\"query\":null,\"topOffset\":0,\"leftOffset\":0}"; }