You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
457 lines
18 KiB
457 lines
18 KiB
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<String, Double> resultMap; |
|
|
|
protected List<String> 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<String, Double> convertResult(String result) { |
|
resultMap = new HashMap<String, Double>(); |
|
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<String, Double> 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<String>(); |
|
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}"; |
|
|
|
} |