/*!
* This program is free software; you can redistribute it and/or modify it under the
* terms of the GNU Lesser General Public License, version 2.1 as published by the Free Software
* Foundation.
*
* You should have received a copy of the GNU Lesser General Public License along with this
* program; if not, you can obtain a copy at http://www.gnu.org/licenses/old-licenses/lgpl-2.1.html
* or from the Free Software Foundation, Inc.,
* 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* Copyright (c) 2002-2013 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.reporting.engine.classic.extensions.datasources.olap4j;
import java.lang.reflect.Array;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Set;
import java.util.regex.PatternSyntaxException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.olap4j.CellSet;
import org.olap4j.CellSetAxis;
import org.olap4j.OlapConnection;
import org.olap4j.OlapException;
import org.olap4j.OlapParameterMetaData;
import org.olap4j.OlapStatement;
import org.olap4j.Position;
import org.olap4j.PreparedOlapStatement;
import org.olap4j.metadata.Cube;
import org.olap4j.metadata.Hierarchy;
import org.olap4j.metadata.Member;
import org.olap4j.type.MemberType;
import org.olap4j.type.NumericType;
import org.olap4j.type.SetType;
import org.olap4j.type.StringType;
import org.olap4j.type.Type;
import org.pentaho.reporting.engine.classic.core.AbstractDataFactory;
import org.pentaho.reporting.engine.classic.core.ClassicEngineBoot;
import org.pentaho.reporting.engine.classic.core.DataFactory;
import org.pentaho.reporting.engine.classic.core.DataFactoryContext;
import org.pentaho.reporting.engine.classic.core.DataRow;
import org.pentaho.reporting.engine.classic.core.ReportDataFactoryException;
import org.pentaho.reporting.engine.classic.core.util.PropertyLookupParser;
import org.pentaho.reporting.engine.classic.extensions.datasources.olap4j.connections.OlapConnectionProvider;
import org.pentaho.reporting.libraries.base.config.Configuration;
import org.pentaho.reporting.libraries.base.util.CSVTokenizer;
import org.pentaho.reporting.libraries.base.util.ObjectUtilities;
import org.pentaho.reporting.libraries.formatting.FastMessageFormat;
public abstract class AbstractMDXDataFactory extends AbstractDataFactory
{
private static final Log logger = LogFactory.getLog(AbstractMDXDataFactory.class);
/**
* The message compiler maps all named references into numeric references.
*/
protected static class MDXCompiler extends PropertyLookupParser
{
private DataRow parameters;
private Locale locale;
private HashSet<String> collectedLists;
/**
* Default Constructor.
*/
protected MDXCompiler(final DataRow parameters,
final Locale locale)
{
this.collectedLists = new HashSet<String>();
this.parameters = parameters;
this.locale = locale;
setMarkerChar('$');
setOpeningBraceChar('{');
setClosingBraceChar('}');
}
/**
* Looks up the property with the given name. This replaces the name with the current index position.
*
* @param name the name of the property to look up.
* @return the translated value.
*/
protected String lookupVariable(final String name)
{
final CSVTokenizer tokenizer = new CSVTokenizer(name, false);
if (tokenizer.hasMoreTokens() == false)
{
return null;
}
final String parameterName = tokenizer.nextToken();
final Object o = parameters.get(parameterName);
collectedLists.add(parameterName);
String subType = null;
final StringBuilder b = new StringBuilder(name.length() + 4);
b.append('{');
b.append("0");
while (tokenizer.hasMoreTokens())
{
b.append(',');
final String token = tokenizer.nextToken();
b.append(token);
if (subType == null)
{
subType = token;
}
}
b.append('}');
final String formatString = b.toString();
if ("string".equals(subType))
{
if (o == null)
{
return "null";
}
return quote(String.valueOf(o));
}
final FastMessageFormat messageFormat = new FastMessageFormat(formatString, locale);
return messageFormat.format(new Object[]{o});
}
public Set<String> getParameter()
{
//noinspection unchecked
return Collections.unmodifiableSet((Set<String>) collectedLists.clone());
}
}
private static final String[] EMPTY_QUERYNAMES = new String[0];
private OlapConnectionProvider connectionProvider;
private transient OlapConnection connection;
private String jdbcUserField;
private String jdbcPasswordField;
private String roleField;
private boolean membersOnAxisSorted;
public AbstractMDXDataFactory(final OlapConnectionProvider connectionProvider)
{
if (connectionProvider == null)
{
throw new NullPointerException();
}
this.connectionProvider = connectionProvider;
}
public void setConnectionProvider(final OlapConnectionProvider connectionProvider)
{
if (connectionProvider == null)
{
throw new NullPointerException();
}
if (connection != null)
{
throw new IllegalStateException();
}
this.connectionProvider = connectionProvider;
}
public OlapConnectionProvider getConnectionProvider()
{
return connectionProvider;
}
public boolean isMembersOnAxisSorted()
{
return membersOnAxisSorted;
}
public void setMembersOnAxisSorted(final boolean membersOnAxisSorted)
{
this.membersOnAxisSorted = membersOnAxisSorted;
}
public String getJdbcUserField()
{
return jdbcUserField;
}
public void setJdbcUserField(final String jdbcUserField)
{
this.jdbcUserField = jdbcUserField;
}
public String getJdbcPasswordField()
{
return jdbcPasswordField;
}
public void setJdbcPasswordField(final String jdbcPasswordField)
{
this.jdbcPasswordField = jdbcPasswordField;
}
public String getRoleField()
{
return roleField;
}
public void setRoleField(final String roleField)
{
this.roleField = roleField;
}
/**
* Checks whether the query would be executable by this datafactory. This performs a rough check, not a full query.
*
* @param query
* @param parameters
* @return
*/
public boolean isQueryExecutable(final String query, final DataRow parameters)
{
return true;
}
public String[] getQueryNames()
{
return EMPTY_QUERYNAMES;
}
protected PreparedOlapStatement getStatement(final String query, final DataRow parameter)
throws ReportDataFactoryException, OlapException
{
if (connection == null)
{
try
{
connection = connectionProvider.createConnection(computeJdbcUser(parameter), computeJdbcPassword(parameter));
connection.setLocale(getLocale());
final String role = computeRole(parameter);
if (role != null)
{
connection.setRoleName(role);
}
}
catch (final SQLException e)
{
throw new ReportDataFactoryException("Failed to obtain a connection", e);
}
}
final MDXCompiler compiler = new MDXCompiler(parameter, getLocale());
final String translatedQuery = compiler.translateAndLookup(query, parameter);
return connection.prepareOlapStatement(translatedQuery);
}
private String computeJdbcUser(final DataRow parameters)
{
if (jdbcUserField != null)
{
final Object field = parameters.get(jdbcUserField);
if (field != null)
{
return String.valueOf(field);
}
}
return null;
}
private String computeJdbcPassword(final DataRow parameters)
{
if (jdbcPasswordField != null)
{
final Object field = parameters.get(jdbcPasswordField);
if (field != null)
{
return String.valueOf(field);
}
}
return null;
}
private String computeRole(final DataRow parameters) throws ReportDataFactoryException
{
if (roleField != null)
{
final Object field = parameters.get(roleField);
if (field != null)
{
if (field instanceof Object[])
{
final Object[] roleArray = (Object[]) field;
final StringBuilder buffer = new StringBuilder();
final int length = roleArray.length;
for (int i = 0; i < length; i++)
{
final Object o = roleArray[i];
if (o == null)
{
continue;
}
final String role = filter(String.valueOf(o));
if (role == null)
{
continue;
}
buffer.append(quoteRole(role));
}
return buffer.toString();
}
else if (field.getClass().isArray())
{
final StringBuilder buffer = new StringBuilder();
final int length = Array.getLength(field);
for (int i = 0; i < length; i++)
{
final Object o = Array.get(field, i);
if (o == null)
{
continue;
}
final String role = filter(String.valueOf(o));
if (role == null)
{
continue;
}
buffer.append(quoteRole(role));
}
return buffer.toString();
}
final String role = filter(String.valueOf(field));
if (role != null)
{
return role;
}
}
}
return null;
}
private String quoteRole(final String role)
{
if (role.indexOf(',') == -1)
{
return role;
}
final StringBuilder b = new StringBuilder(role.length() + 5);
final char[] chars = role.toCharArray();
for (int i = 0; i < chars.length; i++)
{
final char c = chars[i];
if (c == ',')
{
b.append(c);
}
b.append(c);
}
return b.toString();
}
protected QueryResultWrapper performQuery(final String rawMdxQuery, final DataRow parameters)
throws ReportDataFactoryException, SQLException
{
final PreparedOlapStatement statement = getStatement(rawMdxQuery, parameters);
final int queryTimeoutValue = calculateQueryTimeOut(parameters);
if (queryTimeoutValue > 0)
{
statement.setQueryTimeout(queryTimeoutValue);
}
parametrizeQuery(parameters, statement);
return new QueryResultWrapper(statement, statement.executeQuery());
}
private void parametrizeQuery(final DataRow parameters,
final PreparedOlapStatement statement) throws SQLException, ReportDataFactoryException
{
final OlapParameterMetaData olapParameterMetaData = statement.getParameterMetaData();
final int paramCount = olapParameterMetaData.getParameterCount();
for (int i = 1; i <= paramCount; i++)
{
final String paramName = olapParameterMetaData.getParameterName(i);
Object parameterValue = parameters.get(paramName);
final Type parameterType = olapParameterMetaData.getParameterOlapType(i);
parameterValue = computeParameterValue(statement, parameterType, parameterValue);
statement.setObject(i, parameterValue);
}
}
private Object computeParameterValue(final PreparedOlapStatement statement,
final Type parameterType,
Object parameterValue) throws ReportDataFactoryException, SQLException
{
if (parameterValue == null) {
return null;
}
if (parameterType instanceof StringType)
{
if (!(parameterValue instanceof String))
{
throw new ReportDataFactoryException(parameterValue + " is incorrect for type " + parameterType);
}
}
if (parameterType instanceof NumericType)
{
if (!(parameterValue instanceof Number))
{
throw new ReportDataFactoryException(parameterValue + " is incorrect for type " + parameterType);
}
}
if (parameterType instanceof MemberType)
{
if (parameterValue instanceof String)
{
final MemberType type = (MemberType) parameterType;
final Hierarchy hierarchy = type.getHierarchy();
final Cube cube = statement.getCube();
parameterValue = findMember(hierarchy, cube, String.valueOf(parameterValue));
}
else if (!(parameterValue instanceof Member))
{
throw new ReportDataFactoryException(parameterValue + " is incorrect for type " + parameterType);
}
}
if (parameterType instanceof SetType)
{
if (parameterValue instanceof String)
{
final SetType type = (SetType) parameterType;
final Hierarchy hierarchy = type.getHierarchy();
final Cube cube = statement.getCube();
final String rawString = (String) parameterValue;
final String[] memberStr = rawString.replaceFirst("^ *\\{", "").replaceFirst("} *$", "").split(",");
final List<Member> list = new ArrayList<Member>(memberStr.length);
for (int j = 0; j < memberStr.length; j++)
{
final String str = memberStr[j];
final Member member = findMember(hierarchy, cube, String.valueOf(str));
list.add(member);
}
parameterValue = list;
}
else if (!(parameterValue instanceof Member))
{
throw new ReportDataFactoryException(parameterValue + " is incorrect for type " + parameterType);
}
}
return parameterValue;
}
public String[] getReferencedFields(final String queryName,
final DataRow parameter) throws ReportDataFactoryException
{
try
{
if (connection == null)
{
connection = connectionProvider.createConnection(computeJdbcUser(parameter), computeJdbcPassword(parameter));
connection.setLocale(getLocale());
final String role = computeRole(parameter);
if (role != null)
{
connection.setRoleName(role);
}
}
final MDXCompiler compiler = new MDXCompiler(parameter, getLocale());
final String value = computedQuery(queryName, parameter);
final String translatedQuery = compiler.translateAndLookup(value, parameter);
final LinkedHashSet<String> params = new LinkedHashSet<String>();
params.addAll(compiler.getParameter());
if (getRoleField() != null)
{
params.add(getRoleField());
}
if (getJdbcPasswordField() != null)
{
params.add(getJdbcPasswordField());
}
if (getJdbcUserField() != null)
{
params.add(getJdbcUserField());
}
final PreparedOlapStatement statement = connection.prepareOlapStatement(translatedQuery);
final OlapParameterMetaData data = statement.getParameterMetaData();
final int count = data.getParameterCount();
for (int i = 0; i < count; i++)
{
final String parameterName = data.getParameterName(i + 1);
params.add(parameterName);
}
params.add(DataFactory.QUERY_LIMIT);
return params.toArray(new String[params.size()]);
}
catch (final Throwable e)
{
throw new ReportDataFactoryException("Failed to obtain a connection", e);
}
}
private Member findMember(final Hierarchy hierarchy,
final Cube cube,
final String parameter) throws ReportDataFactoryException, SQLException
{
Member memberById = null;
Member memberByUniqueId = null;
final Configuration configuration = getConfiguration();
final boolean searchForNames = "true".equals(configuration.getConfigProperty
("org.pentaho.reporting.engine.classic.extensions.datasources.olap4j.NeedDimensionPrefix")) == false;
final boolean missingMembersIsFatal = "true".equals(configuration.getConfigProperty
("org.pentaho.reporting.engine.classic.extensions.datasources.olap4j.IgnoreInvalidMembersDuringQuery")) == false;
try
{
final Member directValue = lookupDirectly(hierarchy, cube, parameter, searchForNames);
if (directValue != null)
{
return directValue;
}
}
catch (final Exception e)
{
// It is non fatal if that fails. Invalid input has this effect.
}
final OlapStatement statement = connection.createStatement();
try
{
final CellSet result = statement.executeOlapQuery("SELECT " + hierarchy.getUniqueName() +
".AllMembers ON 0, {} ON 1 FROM " + cube.getUniqueName());
try
{
final List<CellSetAxis> setAxises = result.getAxes();
final List<Position> positionList = setAxises.get(0).getPositions();
for (int i = 0; i < positionList.size(); i++)
{
final Position position = positionList.get(i);
final List<Member> memberList = position.getMembers();
for (int j = 0; j < memberList.size(); j++)
{
final Member member = memberList.get(j);
if (parameter.equals(Olap4jUtil.getUniqueMemberName(member)))
{
if (memberByUniqueId == null)
{
memberByUniqueId = member;
}
else
{
logger.warn("Encountered a member with a duplicate unique key: " + member.getUniqueName());
}
}
if (searchForNames == false)
{
continue;
}
if (parameter.equals(member.getName()))
{
if (memberById == null)
{
memberById = member;
}
else
{
logger.warn("Encountered a member with a duplicate name: " + member.getUniqueName());
}
}
}
}
}
finally
{
result.close();
}
}
finally
{
try
{
statement.close();
}
catch (final SQLException e)
{
// ignore
}
}
if (memberByUniqueId != null)
{
return memberByUniqueId;
}
if (memberById != null)
{
return memberById;
}
if (missingMembersIsFatal)
{
throw new ReportDataFactoryException("No member matches parameter value '" + parameter + "'.");
}
return null;
}
private Member lookupDirectly(final Hierarchy hierarchy,
final Cube cube,
final String parameter,
final boolean searchForNames) throws SQLException
{
Member memberById = null;
Member memberByUniqueId = null;
final OlapStatement statement = connection.createStatement();
try
{
final CellSet result = statement.executeOlapQuery("SELECT STRTOMEMBER(" + quote(parameter) +
") ON 0, {} ON 1 FROM " + cube.getUniqueName());
try
{
final List<CellSetAxis> setAxises = result.getAxes();
final List<Position> positionList = setAxises.get(0).getPositions();
for (int i = 0; i < positionList.size(); i++)
{
final Position position = positionList.get(i);
final List<Member> memberList = position.getMembers();
for (int j = 0; j < memberList.size(); j++)
{
final Member member = memberList.get(j);
// If the parameter starts with '[', we'll assume we have the full
// member specification specification. Otherwise, keep the funky lookup
// route. We do check whether we get a second member (heck, should not
// happen, but I've seen pigs fly already).
if (parameter.startsWith("["))
{
if (memberByUniqueId == null)
{
memberByUniqueId = member;
}
else
{
logger.warn("Encountered a member with a duplicate unique key: " + member.getUniqueName());
}
}
if (searchForNames == false)
{
continue;
}
if (parameter.equals(member.getName()))
{
if (memberById == null)
{
memberById = member;
}
else
{
logger.warn("Encountered a member with a duplicate name: " + member.getUniqueName());
}
}
}
}
}
finally
{
result.close();
}
}
finally
{
try
{
statement.close();
}
catch (final SQLException e)
{
// ignore
}
}
if (memberByUniqueId != null)
{
final Hierarchy memberHierarchy = memberByUniqueId.getHierarchy();
if (hierarchy != memberHierarchy)
{
if (ObjectUtilities.equal(hierarchy, memberHierarchy) == false)
{
logger.warn("Cannot match hierarchy of member found with the hierarchy specfied in the parameter: " +
"Unabe to guarantee that the correct member has been queried, returning null.");
return null;
}
}
return memberByUniqueId;
}
if (memberById != null)
{
final Hierarchy memberHierarchy = memberById.getHierarchy();
if (hierarchy != memberHierarchy)
{
if (ObjectUtilities.equal(hierarchy, memberHierarchy) == false)
{
logger.warn("Cannot match hierarchy of member found with the hierarchy specfied in the parameter: " +
"Unabe to guarantee that the correct member has been queried, returning null.");
return null;
}
}
return memberById;
}
return null;
}
protected int extractQueryLimit(final DataRow parameters)
{
final Object queryLimit = parameters.get(DataFactory.QUERY_LIMIT);
final int queryLimitValue;
if (queryLimit instanceof Number)
{
final Number i = (Number) queryLimit;
queryLimitValue = Math.max(0, i.intValue());
}
else
{
// means no limit at all
queryLimitValue = 0;
}
return queryLimitValue;
}
/**
* Closes the data factory and frees all resources held by this instance.
*/
public void close()
{
if (connection != null)
{
try
{
connection.close();
}
catch (final SQLException e)
{
// ignore ..
}
}
connection = null;
}
public AbstractMDXDataFactory clone()
{
final AbstractMDXDataFactory dataFactory = (AbstractMDXDataFactory) super.clone();
dataFactory.connection = null;
return dataFactory;
}
protected static String quote(final String original)
{
// This solution needs improvements. Copy blocks instead of single
// characters.
final int length = original.length();
final StringBuilder b = new StringBuilder(length * 12 / 10);
b.append('"');
for (int i = 0; i < length; i++)
{
final char c = original.charAt(i);
if (c == '"')
{
b.append('"');
b.append('"');
}
else
{
b.append(c);
}
}
b.append('"');
return b.toString();
}
private String filter(final String role) throws ReportDataFactoryException
{
final Configuration configuration = ClassicEngineBoot.getInstance().getGlobalConfig();
if ("true".equals(configuration.getConfigProperty
("org.pentaho.reporting.engine.classic.extensions.datasources.olap4j.role-filter.enable")) == false)
{
return role;
}
final Iterator staticDenyKeys = configuration.findPropertyKeys
("org.pentaho.reporting.engine.classic.extensions.datasources.olap4j.role-filter.static.deny");
while (staticDenyKeys.hasNext())
{
final String key = (String) staticDenyKeys.next();
final String value = configuration.getConfigProperty(key);
if (ObjectUtilities.equal(value, role))
{
return null;
}
}
final Iterator regExpDenyKeys = configuration.findPropertyKeys
("org.pentaho.reporting.engine.classic.extensions.datasources.olap4j.role-filter.reg-exp.deny");
while (regExpDenyKeys.hasNext())
{
final String key = (String) regExpDenyKeys.next();
final String value = configuration.getConfigProperty(key);
try
{
if (role.matches(value))
{
return null;
}
}
catch (final PatternSyntaxException pe)
{
throw new ReportDataFactoryException("Unable to match reg-exp role filter:", pe);
}
}
boolean hasAccept = false;
final Iterator staticAcceptKeys = configuration.findPropertyKeys
("org.pentaho.reporting.engine.classic.extensions.datasources.olap4j.role-filter.static.accept");
while (staticAcceptKeys.hasNext())
{
hasAccept = true;
final String key = (String) staticAcceptKeys.next();
final String value = configuration.getConfigProperty(key);
if (ObjectUtilities.equal(value, role))
{
return role;
}
}
final Iterator regExpAcceptKeys = configuration.findPropertyKeys
("org.pentaho.reporting.engine.classic.extensions.datasources.olap4j.role-filter.reg-exp.accept");
while (regExpAcceptKeys.hasNext())
{
hasAccept = true;
final String key = (String) regExpAcceptKeys.next();
final String value = configuration.getConfigProperty(key);
try
{
if (role.matches(value))
{
return role;
}
}
catch (final PatternSyntaxException pe)
{
throw new ReportDataFactoryException("Unable to match reg-exp role filter:", pe);
}
}
if (hasAccept == false)
{
return role;
}
return null;
}
protected String computedQuery(final String queryName, final DataRow parameters) throws ReportDataFactoryException
{
return queryName;
}
protected String translateQuery(final String queryName)
{
return queryName;
}
public ArrayList<Object> getQueryHash(final String queryRaw, final DataRow parameter)
throws ReportDataFactoryException
{
final Object connection = getConnectionProvider().getConnectionHash();
final ArrayList<Object> list = new ArrayList<Object>();
list.add(getClass().getName());
list.add(translateQuery(queryRaw));
list.add(connection);
return list;
}
public void initialize(final DataFactoryContext dataFactoryContext) throws ReportDataFactoryException
{
super.initialize(dataFactoryContext);
membersOnAxisSorted = "true".equals
(dataFactoryContext.getConfiguration().getConfigProperty(Olap4JDataFactoryModule.MEMBER_ON_AXIS_SORTED_KEY));
}
}