/*!
* Copyright 2002 - 2014 Webdetails, a Pentaho company. All rights reserved.
*
* This software was developed by Webdetails and is provided under the terms
* of the Mozilla Public License, Version 2.0, or any later version. You may not use
* this file except in compliance with the license. If you need a copy of the license,
* please go to http://mozilla.org/MPL/2.0/. The Initial Developer is Webdetails.
*
* Software distributed under the Mozilla Public License is distributed on an "AS IS"
* basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. Please refer to
* the license for the specific language governing your rights and limitations.
*/
package org.pentaho.cdf.context;
import java.io.OutputStream;
import java.io.StringReader;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.dom4j.Document;
import org.dom4j.DocumentException;
import org.dom4j.Node;
import org.dom4j.io.SAXReader;
import org.json.JSONException;
import org.json.JSONObject;
import org.pentaho.cdf.CdfConstants;
import org.pentaho.cdf.context.autoinclude.AutoInclude;
import org.pentaho.cdf.environment.CdfEngine;
import org.pentaho.cdf.environment.ICdfEnvironment;
import org.pentaho.cdf.storage.StorageEngine;
import org.pentaho.cdf.util.Parameter;
import org.pentaho.cdf.views.View;
import org.pentaho.cdf.views.ViewsEngine;
import org.pentaho.platform.api.engine.IPentahoSession;
import org.pentaho.platform.engine.core.system.PentahoSessionHolder;
import org.pentaho.platform.engine.security.SecurityParameterProvider;
import pt.webdetails.cpf.InterPluginCall;
import pt.webdetails.cpf.repository.api.IContentAccessFactory;
import pt.webdetails.cpf.repository.api.IReadAccess;
import pt.webdetails.cpf.repository.util.RepositoryHelper;
import pt.webdetails.cpf.utils.PluginIOUtils;
import pt.webdetails.cpf.utils.XmlDom4JUtils;
public class ContextEngine {
private static final Log logger = LogFactory.getLog( ContextEngine.class );
private static final String PREFIX_PARAMETER = "param";
static final String SESSION_PRINCIPAL = "SECURITY_PRINCIPAL";
private static ContextEngine instance;
/* [settings.xml] legacy-dashboard-context: flag indicating if Dashboard.context should assume the
* legacy structure, including deprecated attributes such as: solution, path, file, fullPath, isAdmin
*/
private final static boolean APPLY_LEGACY_DASHBOARD_CONTEXT = Boolean.valueOf(
StringUtils.defaultIfEmpty( CdfEngine.getEnvironment().getResourceLoader()
.getPluginSetting( ContextEngine.class, CdfConstants.PLUGIN_SETTINGS_LEGACY_DASHBOARD_CONTEXT ) , "false" ) );
private static String CONFIG_FILE = "dashboardContext.xml";
private static List<AutoInclude> autoIncludes;
private static Object autoIncludesLock = new Object();
protected IPentahoSession userSession;
public ContextEngine() {
}
public static synchronized ContextEngine getInstance() {
if ( instance == null ) {
instance = new ContextEngine();
}
return instance;
}
protected IPentahoSession getUserSession() {
return PentahoSessionHolder.getSession();
}
public String getContext( String path, String viewId, String action, Map<String, String> parameters,
int inactiveInterval ) {
final JSONObject contextObj = new JSONObject();
Document config = getConfigFile();
try {
String username = getUserSession().getName();
buildContextConfig( contextObj, path, config, username );
buildContextSessionTimeout( contextObj, inactiveInterval );
buildContextDates( contextObj );
contextObj.put( "user", getUserSession().getName() );
contextObj.put( "locale", CdfEngine.getEnvironment().getLocale() );
buildContextPaths( contextObj, path, parameters );
SecurityParameterProvider securityParams = new SecurityParameterProvider( getUserSession() );
contextObj.put( "roles", securityParams.getParameter( "principalRoles" ) );
if ( APPLY_LEGACY_DASHBOARD_CONTEXT ) {
buildLegacyStructure( contextObj, path, securityParams );
}
final JSONObject params = new JSONObject();
buildContextParams( params, parameters );
contextObj.put( "params", params );
logger.info( "[Timing] Finished building context: "
+ ( new SimpleDateFormat( "HH:mm:ss.SSS" ) ).format( new Date() ) );
return buildContextScript( contextObj, viewId, action, username );
} catch ( JSONException e ) {
return "";
}
}
private JSONObject buildContextSessionTimeout( final JSONObject contextObj, int inactiveInterval )
throws JSONException {
if ( getUserSession().isAuthenticated() ) {
contextObj.put( "sessionTimeout", inactiveInterval );
}
return contextObj;
}
private JSONObject buildContextPaths( final JSONObject contextObj, String dashboardPath,
Map<String, String> parameters ) throws JSONException {
contextObj.put( "path", dashboardPath );
if ( parameters != null && parameters.containsKey( Parameter.SOLUTION ) ) {
contextObj.put( Parameter.SOLUTION, parameters.get( Parameter.SOLUTION ) );
} // TODO redo this
return contextObj;
}
private JSONObject buildContextDates( final JSONObject contextObj ) throws JSONException {
Calendar cal = Calendar.getInstance();
long utcTime = cal.getTimeInMillis();
contextObj.put( "serverLocalDate", utcTime + cal.getTimeZone().getOffset( utcTime ) );
contextObj.put( "serverUTCDate", utcTime );
return contextObj;
}
// Maintain backward compatibility. This is a configurable option via plugin's settings.xml
private JSONObject buildLegacyStructure( final JSONObject contextObj, String path, SecurityParameterProvider securityParams )
throws JSONException {
logger.warn( "CDF: using legacy structure for Dashboard.context; " +
"this is a deprecated structure and should not be used. This is a configurable option via plugin's settings.xml" );
if( securityParams != null ){
contextObj.put( "isAdmin", Boolean.valueOf( (String) securityParams.getParameter( "principalAdministrator" ) ) );
}
if( !StringUtils.isEmpty( path ) ) {
if ( !contextObj.has( Parameter.FULL_PATH ) ) {
contextObj.put( Parameter.FULL_PATH, path ); // create fullPath ctx attribute
}
// now parse full path into legacy structure of solution, path, file
if ( path.startsWith( String.valueOf( RepositoryHelper.SEPARATOR ) ) ) {
path = path.replaceFirst( String.valueOf( RepositoryHelper.SEPARATOR ), StringUtils.EMPTY );
}
// we must determine if this is a full path to a folder or to a file
boolean isPathToFile = !StringUtils.isEmpty( FilenameUtils.getExtension( path ) );
if ( isPathToFile ) {
contextObj.put( Parameter.FILE, FilenameUtils.getName( path ) ); // create file ctx attribute
path = path.replace( FilenameUtils.getName( path ), StringUtils.EMPTY ); // remove and continue on
}
path = FilenameUtils.normalizeNoEndSeparator( path );
String[] parsedPath = path.split( String.valueOf( RepositoryHelper.SEPARATOR ) );
if ( parsedPath != null ) {
if ( parsedPath.length == 0 ) {
contextObj.put( Parameter.SOLUTION, StringUtils.EMPTY ); // create solution ctx attribute
contextObj.put( Parameter.PATH, StringUtils.EMPTY ); // create path ctx attribute
} else if ( parsedPath.length == 1 ) {
contextObj.put( Parameter.SOLUTION, parsedPath[ 0 ] ); // create solution ctx attribute
contextObj.put( Parameter.PATH, StringUtils.EMPTY ); // create path ctx attribute
} else {
contextObj.put( Parameter.SOLUTION, parsedPath[ 0 ] ); // create solution ctx attribute
path = path.replace( FilenameUtils.getName( parsedPath[ 0 ] ), StringUtils.EMPTY ); // remove and continue on
path = path.replaceFirst( String.valueOf( RepositoryHelper.SEPARATOR ), StringUtils.EMPTY );
contextObj.put( Parameter.PATH, path ); // create path ctx attribute
}
}
}
return contextObj;
}
private JSONObject buildContextConfig( final JSONObject contextObj, String fullPath, Document config, String user )
throws JSONException {
contextObj.put( "queryData", processAutoIncludes( fullPath, config ) );
contextObj.put( "sessionAttributes", processSessionAttributes( config, user ) );
return contextObj;
}
private String buildContextScript( JSONObject contextObj, String viewId, String action, String user )
throws JSONException {
final StringBuilder s = new StringBuilder();
s.append( "\n<script language=\"javascript\" type=\"text/javascript\">\n" );
s.append( " Dashboards.context = " );
s.append( contextObj.toString( 2 ) + "\n" );
View view = ViewsEngine.getInstance().getView( ( viewId.isEmpty() ? action : viewId ), user );
if ( view != null ) {
s.append( "Dashboards.view = " );
s.append( view.toJSON().toString( 2 ) + "\n" );
}
String storage = getStorage();
if ( !"".equals( storage ) ) {
s.append( "Dashboards.initialStorage = " );
s.append( storage );
s.append( "\n" );
}
s.append( "</script>\n" );
return s.toString();
}
private JSONObject buildContextParams( final JSONObject contextObj, Map<String, String> params )
throws JSONException {
for ( String param : params.values() ) {
if ( param.startsWith( PREFIX_PARAMETER ) ) {
contextObj.put( param.substring( PREFIX_PARAMETER.length() ), params.get( param ) );
}
}
return contextObj;
}
public JSONObject processSessionAttributes( Document config, String user ) {
JSONObject result = new JSONObject();
@SuppressWarnings( "unchecked" )
List<Node> attributes = config.selectNodes( "//sessionattributes/attribute" );
for ( Node attribute : attributes ) {
String name = attribute.getText();
String key = XmlDom4JUtils.getNodeText( "@name", attribute );
if ( key == null ) {
key = name;
}
try {
result.put( key, user );
} catch ( JSONException e ) {
logger.error( e );
}
}
return result;
}
private List<String> listQueries( String cda ) {
SAXReader reader = new SAXReader();
List<String> queryOutput = new ArrayList<String>();
try {
Map<String, Object> params = new HashMap<String, Object>();
params.put( "path", cda );
params.put( "outputType", "xml" );
InterPluginCall ipc = new InterPluginCall( InterPluginCall.CDA, "listQueries", params );
String reply = ipc.call();
Document queryList = reader.read( new StringReader( reply ) );
@SuppressWarnings( "unchecked" )
List<Node> queries = queryList.selectNodes( "//ResultSet/Row/Col[1]" );
for ( Node query : queries ) {
queryOutput.add( query.getText() );
}
} catch ( DocumentException e ) {
return null;
}
return queryOutput;
}
private String getStorage() {
try {
return StorageEngine.getInstance().read( getUserSession().getName() ).toString( 2 );
} catch ( Exception e ) {
logger.error( e );
return "";
}
}
/**
* will add a json entry for each data access id in the cda queries applicable to currents dashboard.
*/
private JSONObject processAutoIncludes( String dashboardPath, Document config ) {
JSONObject queries = new JSONObject();
/* Bail out immediately if CDA isn' available */
if ( !( new InterPluginCall( InterPluginCall.CDA, "" ) ).pluginExists() ) {
logger.warn( "Couldn't find CDA. Skipping auto-includes" );
return queries;
}
/* Bail out if cdf/includes folder does not exists */
IReadAccess autoIncludesFolder = CdfEngine.getUserContentReader( null );
if ( !autoIncludesFolder.fileExists(
CdfEngine.getEnvironment().getCdfPluginRepositoryDir() + CdfConstants.INCLUDES_DIR ) ) {
return queries;
}
List<AutoInclude> autoIncludes = getAutoIncludes( config );
for ( AutoInclude autoInclude : autoIncludes ) {
if ( autoInclude.canInclude( dashboardPath ) ) {
String cdaPath = autoInclude.getCdaPath();
CdfEngine.getEnvironment().getCdfInterPluginBroker().addCdaQueries( queries, cdaPath );
}
}
return queries;
}
private List<AutoInclude> getAutoIncludes( Document config ) {
synchronized( autoIncludesLock ) {
if ( autoIncludes == null ) {
IReadAccess cdaRoot =
CdfEngine.getUserContentReader( CdfEngine.getEnvironment().getCdfPluginRepositoryDir()
+ CdfConstants.INCLUDES_DIR );
autoIncludes = AutoInclude.buildAutoIncludeList( config, cdaRoot );
}
return autoIncludes;
}
}
private Document getConfigFile() {
try {
IContentAccessFactory factory = CdfEngine.getEnvironment().getContentAccessFactory();
IReadAccess access = factory.getPluginRepositoryReader( null );
if ( !access.fileExists( CONFIG_FILE ) ) {
access = factory.getPluginSystemReader( null );
if ( !access.fileExists( CONFIG_FILE ) ) {
logger.error( CONFIG_FILE + " not found!" );
return null;
}
}
if ( logger.isDebugEnabled() ) {
logger.debug( String.format( "Reading %s from %s", CONFIG_FILE, access ) );
}
return XmlDom4JUtils.getDocumentFromStream( access.getFileInputStream( CONFIG_FILE ) );
} catch ( Exception e ) {
logger.error( "Couldn't read context configuration file.", e );
return null;
}
}
public static void clearCache() {
// TODO figure out what to clear
synchronized( autoIncludesLock ) {
autoIncludes = null;
logger.debug( "auto-includes cleared." );
}
}
public static void generateContext( final OutputStream out, HashMap<String, String> paramMap, int inactiveInterval )
throws Exception {
String solution = StringUtils.defaultIfEmpty( paramMap.get( Parameter.SOLUTION ), StringUtils.EMPTY );
String path = StringUtils.defaultIfEmpty( paramMap.get( Parameter.PATH ), StringUtils.EMPTY );
String file = StringUtils.defaultIfEmpty( paramMap.get( Parameter.FILE ), StringUtils.EMPTY );
String action = StringUtils.defaultIfEmpty( paramMap.get( Parameter.ACTION ), StringUtils.EMPTY );
// TODO: why does view default to action?
String viewId = StringUtils.defaultIfEmpty( paramMap.get( Parameter.VIEW ), action );
String fullPath = RepositoryHelper.joinPaths( solution, path, file );
// old xcdf dashboards use solution + path + action
if ( RepositoryHelper.getExtension( action ).equals( "xcdf" ) ) {
fullPath = RepositoryHelper.joinPaths( fullPath, action );
}
String dashboardContext =
ContextEngine.getInstance().getContext( fullPath, viewId, action, paramMap, inactiveInterval );
if ( StringUtils.isEmpty( dashboardContext ) ) {
logger.error( "empty dashboardContext" );
}
PluginIOUtils.writeOut( out, dashboardContext );
}
}