/*!
* 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-2015 Pentaho Corporation.. All rights reserved.
*/
package org.pentaho.platform.dataaccess.datasource.api.resources;
import com.sun.jersey.core.header.FormDataContentDisposition;
import com.sun.jersey.multipart.FormDataParam;
import org.codehaus.enunciate.Facet;
import org.codehaus.enunciate.jaxrs.ResponseCode;
import org.codehaus.enunciate.jaxrs.StatusCodes;
import org.pentaho.platform.api.engine.PentahoAccessControlException;
import org.pentaho.platform.dataaccess.datasource.api.AnalysisService;
import org.pentaho.platform.plugin.services.importer.PlatformImportException;
import org.pentaho.platform.repository2.unified.webservices.RepositoryFileAclDto;
import org.pentaho.platform.web.http.api.resources.JaxbList;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ws.rs.Consumes;
import javax.ws.rs.DELETE;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.List;
import java.util.Map;
import static javax.ws.rs.core.MediaType.*;
import static javax.ws.rs.core.Response.Status.*;
/**
* This service allows for listing, download, upload, and removal of Analysis files or Mondrian schemas in the BA
* Platform.
*/
@Path( "/data-access/api/datasource/analysis" )
public class AnalysisResource {
protected static final String UPLOAD_ANALYSIS = "uploadInput";
protected static final String CATALOG_ID = "catalogId";
protected static final String ORIG_CATALOG_NAME = "origCatalogName";
protected static final String DATASOURCE_NAME = "datasourceName";
protected static final String OVERWRITE_IN_REPOS = "overwrite";
protected static final String XMLA_ENABLED_FLAG = "xmlaEnabledFlag";
protected static final String PARAMETERS = "parameters";
private static final String DATASOURCE_ACL = "acl";
private static final int SUCCESS = 3;
protected static final Logger logger = LoggerFactory.getLogger( AnalysisResource.class );
protected AnalysisService service;
protected ResourceUtil resourceUtil;
public AnalysisResource() {
service = createAnalysisService();
resourceUtil = new ResourceUtil();
}
protected AnalysisService createAnalysisService() {
return new AnalysisService();
}
/**
* Download the analysis files for a given analysis id.
*
* <p><b>Example Request:</b><br />
* GET pentaho/plugin/data-access/api/datasource/analysis/catalog/SampleSchema
* </p>
*
* @param catalog String Id of the analysis data to retrieve.
*
* @return Response containing the analysis file data XML.
*
* <p><b>Example Response:</b></p>
* <pre function="syntax.xml">
* <?xml version="1.0"?>
* <Schema name="SampleData2">
* <Dimension name="Region">
* <Hierarchy hasAll="true" allMemberName="All Regions">
* <Table name="QUADRANT_ACTUALS"/>
* <Level name="Region" column="REGION" uniqueMembers="true"/>
* </Hierarchy>
* </Dimension>
* <Dimension name="Department">
* <Hierarchy hasAll="true" allMemberName="All Departments">
* <Table name="QUADRANT_ACTUALS"/>
* <Level name="Department" column="DEPARTMENT" uniqueMembers="true"/>
* </Hierarchy>
* </Dimension>
* <Dimension name="Positions">
* <Hierarchy hasAll="true" allMemberName="All Positions">
* <Table name="QUADRANT_ACTUALS"/>
* <Level name="Positions" column="POSITIONTITLE" uniqueMembers="true"/>
* </Hierarchy>
* </Dimension>
* <Cube name="Quadrant Analysis">
* <Table name="QUADRANT_ACTUALS"/>
* <DimensionUsage name="Region" source="Region"/>
* <DimensionUsage name="Department" source="Department" />
* <DimensionUsage name="Positions" source="Positions" />
* <Measure name="Actual" column="ACTUAL" aggregator="sum" formatString="#,###.00"/>
* <Measure name="Budget" column="BUDGET" aggregator="sum" formatString="#,###.00"/>
* <Measure name="Variance" column="VARIANCE" aggregator="sum" formatString="#,###.00"/>
* </Cube>
* </Schema>
* </pre>
*/
@GET
@Path( "/catalog/{catalogId : .+}" )
@Produces( WILDCARD )
@StatusCodes( {
@ResponseCode( code = 200, condition = "Successfully downloaded the analysis file" ),
@ResponseCode( code = 401, condition = "Unauthorized" ),
@ResponseCode( code = 500, condition = "Unabled to download analysis file" )
} )
public Response downloadSchema( @PathParam( CATALOG_ID ) String catalog ) {
try {
Map<String, InputStream> fileData = service.doGetAnalysisFilesAsDownload( catalog );
return createAttachment( fileData, catalog );
} catch ( PentahoAccessControlException e ) {
throw new WebApplicationException( Response.Status.UNAUTHORIZED );
}
}
/**
* Remove the analysis data for a given analysis ID.
*
* <p><b>Example Request:</b><br />
* DELETE pentaho/plugin/data-access/api/datasource/analysis/catalog/{catalog}
*
* @param catalog ID of the analysis data to remove.
*
* @return A 200 response code representing the successful removal of the analysis datasource.
*
* <p><b>Example Response:</b></p>
* <pre function="syntax.xml">
* This response does not contain data.
* </pre>
*/
@DELETE
@Path( "/catalog/{catalogId : .+}" )
@Produces( WILDCARD )
@StatusCodes( {
@ResponseCode( code = 200, condition = "Successfully removed the analysis data" ),
@ResponseCode( code = 401, condition = "User is not authorized to delete the analysis datasource" ),
@ResponseCode( code = 500, condition = "Unable to remove the analysis data." )
} )
public Response deleteSchema( @PathParam( CATALOG_ID ) String catalog ) {
try {
service.removeAnalysis( catalog );
return buildOkResponse();
} catch ( PentahoAccessControlException e ) {
throw new WebApplicationException( Response.Status.UNAUTHORIZED );
}
}
/**
* Get a list of analysis data source ids.
*
* <p><b>Example Request:</b><br />
* GET pentaho/plugin/data-access/api/datasource/analysis/catalog
* </p>
*
* @return A list of catalog IDs.
*
* <p><b>Example Response:</b></p>
* <pre function="syntax.xml">
* {
* "Item":[
* {
* "@type":"xs:string",
* "$":"SampleData"
* },
* {
* "@type":"xs:string",
* "$":"SteelWheels"
* },
* {
* "@type":"xs:string",
* "$":"pentaho_operations_mart"
* }
* ]
* }
* </pre>
*/
@GET
@Path( "/catalog" )
@Produces( { APPLICATION_XML, APPLICATION_JSON } )
@StatusCodes( {
@ResponseCode( code = 200, condition = "Successfully retrieved the list of analysis IDs" )
} )
public JaxbList<String> getSchemaIds() {
return createNewJaxbList( service.getAnalysisDatasourceIds() );
}
/**
* Import Analysis Schema.
*
* <p><b>Example Request:</b><br />
* PUT pentaho/plugin/data-access/api/datasource/analysis/catalog/SampleSchema
* <br /><b>PUT data:</b>
* <pre function="syntax.xml">
* ------WebKitFormBoundaryNLNb246RTFIn1elY
* Content-Disposition: form-data; name="uploadAnalysis"; filename="SampleData2.mondrian.xml"
* Content-Type: text/xml
*
* <?xml version="1.0"?>
* <Schema name="SampleData2">
* <!-- Shared dimensions -->
*
* <Dimension name="Region">
* <Hierarchy hasAll="true" allMemberName="All Regions">
* <Table name="QUADRANT_ACTUALS"/>
* <Level name="Region" column="REGION" uniqueMembers="true"/>
* </Hierarchy>
* </Dimension>
* <Dimension name="Department">
* <Hierarchy hasAll="true" allMemberName="All Departments">
* <Table name="QUADRANT_ACTUALS"/>
* <Level name="Department" column="DEPARTMENT" uniqueMembers="true"/>
* </Hierarchy>
* </Dimension>
*
* <Dimension name="Positions">
* <Hierarchy hasAll="true" allMemberName="All Positions">
* <Table name="QUADRANT_ACTUALS"/>
* <Level name="Positions" column="POSITIONTITLE" uniqueMembers="true"/>
* </Hierarchy>
* </Dimension>
*
* <Cube name="Quadrant Analysis">
* <Table name="QUADRANT_ACTUALS"/>
* <DimensionUsage name="Region" source="Region"/>
* <DimensionUsage name="Department" source="Department" />
* <DimensionUsage name="Positions" source="Positions" />
* <Measure name="Actual" column="ACTUAL" aggregator="sum" formatString="#,###.00"/>
* <Measure name="Budget" column="BUDGET" aggregator="sum" formatString="#,###.00"/>
* <Measure name="Variance" column="VARIANCE" aggregator="sum" formatString="#,###.00"/>
* <!-- <CalculatedMember name="Variance Percent" dimension="Measures" formula="([Measures].[Variance]/[Measures].[Budget])*100" /> -->
* </Cube>
*
* </Schema>
*
* ------WebKitFormBoundaryNLNb246RTFIn1elY
* Content-Disposition: form-data; name="parameters"
*
* DataSource=SampleData2;overwrite=true
* ------WebKitFormBoundaryNLNb246RTFIn1elY
* Content-Disposition: form-data; name="schemaFileInfo"
*
* test.xml
* ------WebKitFormBoundaryNLNb246RTFIn1elY
* Content-Disposition: form-data; name="catalogName"
*
* Catalog Name
* ------WebKitFormBoundaryNLNb246RTFIn1elY
* Content-Disposition: form-data; name="xmlaEnabledFlag"
*
* true
* ------WebKitFormBoundaryNLNb246RTFIn1elY--
* </pre>
* </p>
*
* @param catalog (optional) The catalog name.
* @param uploadAnalysis A Mondrian schema XML file.
* @param schemaFileInfo User selected name for the file.
* @param origCatalogName (optional) The original catalog name.
* @param datasourceName (optional) The datasource name.
* @param overwrite Flag for overwriting existing version of the file.
* @param xmlaEnabledFlag Is XMLA enabled or not.
* @param parameters Import parameters.
* @param acl acl information for the data source. This parameter is optional.
*
* @return Response containing the success of the method.
*
* <p><b>Example Response:</b></p>
* <pre function="syntax.xml">
* 200
* </pre>
*/
@PUT
@Path( "/catalog/{catalogId : .+}" )
@Consumes( MediaType.MULTIPART_FORM_DATA )
@Produces( "text/plain" )
@StatusCodes( {
@ResponseCode( code = 409, condition = "Content already exists (use overwrite flag to force)" ),
@ResponseCode( code = 401, condition = "Import failed because publish is prohibited" ),
@ResponseCode( code = 500, condition = "Unspecified general error has occurred" ),
@ResponseCode( code = 412,
condition = "Analysis datasource import failed. Error code or message included in response entity" ),
@ResponseCode( code = 403, condition = "Access Control Forbidden" ),
@ResponseCode( code = 201, condition = "Indicates successful import" ) } )
public Response putSchema(
@PathParam( CATALOG_ID ) String catalog, // Optional
@FormDataParam( UPLOAD_ANALYSIS ) InputStream uploadAnalysis,
@FormDataParam( UPLOAD_ANALYSIS ) FormDataContentDisposition schemaFileInfo,
@FormDataParam( ORIG_CATALOG_NAME ) String origCatalogName, // Optional
@FormDataParam( DATASOURCE_NAME ) String datasourceName, // Optional
@FormDataParam( OVERWRITE_IN_REPOS ) Boolean overwrite,
@FormDataParam( XMLA_ENABLED_FLAG ) Boolean xmlaEnabledFlag, @FormDataParam( PARAMETERS ) String parameters,
@FormDataParam( DATASOURCE_ACL ) RepositoryFileAclDto acl )
throws PentahoAccessControlException {
try {
service.putMondrianSchema( uploadAnalysis, schemaFileInfo, catalog, origCatalogName, datasourceName, overwrite,
xmlaEnabledFlag, parameters, acl );
Response response = Response.status( CREATED ).build();
logger.debug( "putMondrianSchema Response " + response );
return response;
} catch ( PentahoAccessControlException pac ) {
int statusCode = PlatformImportException.PUBLISH_USERNAME_PASSWORD_FAIL;
logger.error( "Error putMondrianSchema " + pac.getMessage() + " status = " + statusCode );
throw new ResourceUtil.AccessControlException( pac.getMessage() );
} catch ( PlatformImportException pe ) {
if ( pe.getErrorStatus() == PlatformImportException.PUBLISH_PROHIBITED_SYMBOLS_ERROR ) {
throw new ResourceUtil.PublishProhibitedException( pe.getMessage() );
} else {
String msg = pe.getMessage();
logger.error( "Error import analysis: " + msg + " status = " + pe.getErrorStatus() );
Throwable throwable = pe.getCause();
if ( throwable != null ) {
msg = throwable.getMessage();
logger.error( "Root cause: " + msg );
}
int status = pe.getErrorStatus();
if ( status == 8 ) {
throw new ResourceUtil.ContentAlreadyExistsException( msg );
} else {
throw new ResourceUtil.ImportFailedException( msg );
}
}
} catch ( Exception e ) {
int statusCode = PlatformImportException.PUBLISH_GENERAL_ERROR;
logger.error( "Error putMondrianSchema " + e.getMessage() + " status = " + statusCode );
throw new ResourceUtil.UnspecifiedErrorException( e.getMessage() );
}
}
public Response importMondrianSchema(
@FormDataParam( UPLOAD_ANALYSIS ) InputStream uploadAnalysis,
@FormDataParam( UPLOAD_ANALYSIS ) FormDataContentDisposition schemaFileInfo,
@FormDataParam( CATALOG_ID ) String catalogName, // Optional
@FormDataParam( ORIG_CATALOG_NAME ) String origCatalogName, // Optional
@FormDataParam( DATASOURCE_NAME ) String datasourceName, // Optional
@FormDataParam( OVERWRITE_IN_REPOS ) String overwrite,
@FormDataParam( XMLA_ENABLED_FLAG ) String xmlaEnabledFlag, @FormDataParam( PARAMETERS ) String parameters,
@FormDataParam( DATASOURCE_ACL ) RepositoryFileAclDto acl )
throws PentahoAccessControlException {
Response response = null;
int statusCode = PlatformImportException.PUBLISH_GENERAL_ERROR;
try {
boolean overWriteInRepository = "True".equalsIgnoreCase( overwrite ) ? true : false;
boolean xmlaEnabled = "True".equalsIgnoreCase( xmlaEnabledFlag ) ? true : false;
service.putMondrianSchema( uploadAnalysis, schemaFileInfo, catalogName, origCatalogName, datasourceName,
overWriteInRepository, xmlaEnabled, parameters, acl );
statusCode = SUCCESS;
} catch ( PentahoAccessControlException pac ) {
logger.error( pac.getMessage() );
statusCode = PlatformImportException.PUBLISH_USERNAME_PASSWORD_FAIL;
} catch ( PlatformImportException pe ) {
statusCode = pe.getErrorStatus();
logger.error( "Error putMondrianSchema " + pe.getMessage() + " status = " + statusCode );
} catch ( Exception e ) {
logger.error( "Error putMondrianSchema " + e.getMessage() );
statusCode = PlatformImportException.PUBLISH_GENERAL_ERROR;
}
response = buildOkResponse( String.valueOf( statusCode ) );
logger.debug( "putMondrianSchema Response " + response );
return response;
}
protected JaxbList<String> createNewJaxbList( List<String> DSWDatasources ) {
return new JaxbList<String>( DSWDatasources );
}
protected Response buildOkResponse( String statusCode ) {
return Response.ok( statusCode ).type( MediaType.TEXT_PLAIN ).build();
}
protected Response createAttachment( Map<String, InputStream> fileData, String catalog ) {
return resourceUtil.createAttachment( fileData, catalog );
}
protected Response buildOkResponse() {
return Response.ok().build();
}
protected Response buildUnauthorizedResponse() {
return Response.status( UNAUTHORIZED ).build();
}
protected Response buildServerErrorResponse() {
return Response.serverError().build();
}
/**
* Get list of IDs of analysis datasource
*
* @return JaxbList<String> of analysis IDs
*/
@GET
@Path( "/ids" )
@Produces( { APPLICATION_XML, APPLICATION_JSON } )
@Facet( name = "Unsupported" )
public JaxbList<String> getAnalysisDatasourceIds() {
return getSchemaIds();
}
@GET
@Path( "/{catalog : .+}/download" )
@Produces( WILDCARD )
@StatusCodes( {
@ResponseCode( code = 200, condition = "Successfully downloaded the analysis file" ),
@ResponseCode( code = 401, condition = "Unauthorized" ),
@ResponseCode( code = 500, condition = "Unabled to download analysis file" )
} )
public Response doGetAnalysisFilesAsDownload( @PathParam( "catalog" ) String catalog ) {
return downloadSchema( catalog );
}
@PUT
@Path( "/import" )
@Consumes( MediaType.MULTIPART_FORM_DATA )
@Produces( "text/plain" )
@StatusCodes( {
@ResponseCode( code = 200,
condition = "Status code indicating a success or failure while importing Mondrian schema XML. A response of:\n"
+ " * 2: Unspecified general error has occurred\n"
+ " * 3: Success\n"
+ " * 5: Authorization error" )
} )
public Response putMondrianSchema(
@FormDataParam( UPLOAD_ANALYSIS ) InputStream uploadAnalysis,
@FormDataParam( UPLOAD_ANALYSIS ) FormDataContentDisposition schemaFileInfo,
@FormDataParam( CATALOG_ID ) String catalogName, // Optional
@FormDataParam( ORIG_CATALOG_NAME ) String origCatalogName, // Optional
@FormDataParam( DATASOURCE_NAME ) String datasourceName, // Optional
@FormDataParam( OVERWRITE_IN_REPOS ) String overwrite,
@FormDataParam( XMLA_ENABLED_FLAG ) String xmlaEnabledFlag, @FormDataParam( PARAMETERS ) String parameters,
@FormDataParam( DATASOURCE_ACL ) RepositoryFileAclDto acl )
throws PentahoAccessControlException {
return importMondrianSchema( uploadAnalysis, schemaFileInfo, catalogName, origCatalogName, datasourceName,
overwrite, xmlaEnabledFlag, parameters, acl );
}
@POST
@Path( "/{catalog : .+}/remove" )
@Produces( WILDCARD )
@StatusCodes( {
@ResponseCode( code = 200, condition = "Successfully removed the analysis data" ),
@ResponseCode( code = 401, condition = "User is not authorized to delete the analysis datasource" ),
@ResponseCode( code = 500, condition = "Unable to remove the analysis data." )
} )
public Response doRemoveAnalysis( @PathParam( "catalog" ) String catalog ) {
try {
service.removeAnalysis( catalog );
return buildOkResponse();
} catch ( PentahoAccessControlException e ) {
return buildUnauthorizedResponse();
}
}
/**
* Get ACL for the analysis data source by name
*
* @param catalog analysis data source name
* @return ACL or null if the data source doesn't have it
* @throws PentahoAccessControlException if the user doesn't have access
*/
@GET
@Path( "/{catalog : .+}/acl" )
@Produces ( { APPLICATION_XML, APPLICATION_JSON } )
@StatusCodes( {
@ResponseCode( code = 200, condition = "Successfully got the ACL" ),
@ResponseCode( code = 401, condition = "Unauthorized" ),
@ResponseCode( code = 404, condition = "ACL doesn't exist" ),
@ResponseCode( code = 409, condition = "Analysis DS doesn't exist" ),
@ResponseCode(
code = 500,
condition = "ACL failed to be retrieved. This could be caused by an invalid path, or the file does not exist."
)
} )
public RepositoryFileAclDto doGetAnalysisDatasourceAcl( @PathParam( "catalog" ) String catalog ) {
try {
final RepositoryFileAclDto acl = service.getAnalysisDatasourceAcl( catalog );
if ( acl == null ) {
throw new WebApplicationException( NOT_FOUND );
}
return acl;
} catch ( FileNotFoundException e ) {
throw new WebApplicationException( CONFLICT );
} catch ( PentahoAccessControlException e ) {
throw new WebApplicationException( UNAUTHORIZED );
}
}
/**
* Set ACL for the analysis data source
*
* @param catalog analysis data source name
* @param acl ACL to set
* @return response
* @throws PentahoAccessControlException if the user doesn't have access
*/
@PUT
@Path( "/{catalog : .+}/acl" )
@Produces ( { APPLICATION_XML, APPLICATION_JSON } )
@StatusCodes( {
@ResponseCode( code = 200, condition = "Successfully updated the ACL" ),
@ResponseCode( code = 401, condition = "Unauthorized" ),
@ResponseCode( code = 409, condition = "Analysis DS doesn't exist" ),
@ResponseCode( code = 500, condition = "Failed to save acls due to another error." )
} )
public Response doSetAnalysisDatasourceAcl( @PathParam( "catalog" ) String catalog, RepositoryFileAclDto acl )
throws PentahoAccessControlException {
try {
service.setAnalysisDatasourceAcl( catalog, acl );
return buildOkResponse();
} catch ( PentahoAccessControlException e ) {
return buildUnauthorizedResponse();
} catch ( FileNotFoundException e ) {
return Response.status( CONFLICT ).build();
} catch ( Exception e ) {
return buildServerErrorResponse();
}
}
}