package org.rakam.plugin.user; import com.facebook.presto.sql.parser.SqlParser; import com.facebook.presto.sql.tree.Expression; import com.fasterxml.jackson.annotation.JsonCreator; import com.google.common.base.Throwables; import com.google.inject.Inject; import io.netty.buffer.Unpooled; import io.netty.channel.ChannelFuture; import io.netty.channel.ChannelFutureListener; import io.netty.handler.codec.http.DefaultHttpResponse; import io.netty.handler.codec.http.HttpHeaders; import io.netty.handler.codec.http.HttpResponse; import io.netty.handler.codec.http.HttpResponseStatus; import org.apache.poi.hssf.usermodel.HSSFCellStyle; import org.apache.poi.hssf.usermodel.HSSFFont; import org.apache.poi.hssf.usermodel.HSSFSheet; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Cell; import org.apache.poi.ss.usermodel.CellStyle; import org.apache.poi.ss.usermodel.IndexedColors; import org.apache.poi.ss.usermodel.Row; import org.rakam.analysis.ApiKeyService; import org.rakam.collection.FieldType; import org.rakam.collection.SchemaField; import org.rakam.report.QueryResult; import org.rakam.server.http.HttpServer; import org.rakam.server.http.HttpService; import org.rakam.server.http.RakamHttpRequest; import org.rakam.server.http.annotations.ApiParam; import org.rakam.server.http.annotations.IgnoreApi; import org.rakam.util.ExportUtil; import org.rakam.util.JsonHelper; import org.rakam.util.RakamException; import javax.ws.rs.GET; import javax.ws.rs.Path; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.concurrent.CompletableFuture; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TRANSFER_ENCODING; import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE; import static io.netty.handler.codec.http.HttpHeaders.Names.EXPIRES; import static io.netty.handler.codec.http.HttpResponseStatus.BAD_REQUEST; import static io.netty.handler.codec.http.HttpResponseStatus.FORBIDDEN; import static io.netty.handler.codec.http.HttpResponseStatus.OK; import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1; import static java.lang.String.format; import static org.rakam.analysis.ApiKeyService.AccessKeyType.READ_KEY; @Path("/user/export") @IgnoreApi public class UserUtilHttpService extends HttpService { private final SqlParser sqlParser = new SqlParser(); private final AbstractUserService service; private final ApiKeyService apiKeyService; @Inject public UserUtilHttpService(ApiKeyService apiKeyService, AbstractUserService service) { this.service = service; this.apiKeyService = apiKeyService; } public static class FilterQuery { public final String filter; public final List<UserStorage.EventFilter> event_filter; public final UserStorage.Sorting sorting; @JsonCreator public FilterQuery(@ApiParam(value = "filter", required = false) String filter, @ApiParam(value = "event_filters", required = false) List<UserStorage.EventFilter> event_filter, @ApiParam(value = "sorting", required = false) UserStorage.Sorting sorting) { this.filter = filter; this.event_filter = event_filter; this.sorting = sorting; } } @GET @Path("/") public void export(RakamHttpRequest request) { final Map<String, List<String>> params = request.params(); final List<String> query = params.get("query"); if (query.isEmpty()) { HttpServer.returnError(request, BAD_REQUEST.reasonPhrase(), BAD_REQUEST); return; } final List<String> readKey = params.get("read_key"); if (readKey == null || readKey.isEmpty()) { HttpServer.returnError(request, FORBIDDEN.reasonPhrase(), FORBIDDEN); return; } String body = query.get(0); final ExportQuery read; try { read = JsonHelper.readSafe(body, ExportQuery.class); } catch (IOException e) { HttpServer.returnError(request, "Couldn't parse body: " + e.getMessage(), BAD_REQUEST); return; } String project = apiKeyService.getProjectOfApiKey(readKey.get(0), READ_KEY); Expression expression; if (read.filterQuery.filter != null) { try { synchronized (sqlParser) { expression = sqlParser.createExpression(read.filterQuery.filter); } } catch (Exception e) { throw new RakamException(format("filter expression '%s' couldn't parsed", read.filterQuery.filter), HttpResponseStatus.BAD_REQUEST); } } else { expression = null; } final CompletableFuture<QueryResult> search = service.searchUsers(project, null, expression, read.filterQuery.event_filter, read.filterQuery.sorting, 100000, null); final CompletableFuture<byte[]> stream; switch (read.exportFormat) { case XLS: stream = exportAsExcel(search); break; case CSV: stream = search.thenApply(ExportUtil::exportAsCSV); break; default: throw new IllegalStateException(); } stream.whenComplete((result, ex) -> { if (ex != null) { HttpServer.returnError(request, "Couldn't generate file: " + ex.getMessage(), BAD_REQUEST); return; } HttpResponse response = new DefaultHttpResponse(HTTP_1_1, OK); HttpHeaders.setContentLength(response, result.length); response.headers().set(CONTENT_TYPE, "application/octet-stream"); response.headers().set(EXPIRES, "0"); response.headers().set(CONTENT_TRANSFER_ENCODING, "binary"); response.headers().set("Content-Disposition", "attachment;filename=\"exported_people." + read.exportFormat.name().toLowerCase(Locale.ENGLISH) + "\""); request.context().write(response); ChannelFuture lastContentFuture = request.context().writeAndFlush(Unpooled.wrappedBuffer(result)); if (!HttpHeaders.isKeepAlive(request)) { lastContentFuture.addListener(ChannelFutureListener.CLOSE); } }); } private static class ExportQuery { public final FilterQuery filterQuery; public final ExportFormat exportFormat; @JsonCreator public ExportQuery(@ApiParam("filter") FilterQuery filterQuery, @ApiParam("export_format") ExportFormat exportFormat) { this.filterQuery = filterQuery; this.exportFormat = exportFormat; } } public enum ExportFormat { XLS, CSV; @JsonCreator public static ExportFormat fromString(String str) { return valueOf(str.toUpperCase(Locale.ENGLISH)); } } private CompletableFuture<byte[]> exportAsExcel(CompletableFuture<QueryResult> queryResult) { return queryResult.thenApply(result -> { HSSFWorkbook workbook = new HSSFWorkbook(); HSSFSheet sheet = workbook.createSheet("Users generated by Rakam"); final List<SchemaField> metadata = result.getMetadata(); HSSFFont boldFont = workbook.createFont(); boldFont.setBoldweight(HSSFFont.BOLDWEIGHT_BOLD); HSSFCellStyle headerSyle = workbook.createCellStyle(); headerSyle.setBorderBottom(CellStyle.BORDER_THIN); headerSyle.setBottomBorderColor(IndexedColors.BLACK.getIndex()); headerSyle.setFont(boldFont); Row headerRow = sheet.createRow(0); headerRow.setRowStyle(headerSyle); for (int i = 0; i < metadata.size(); i++) { final Cell cell = headerRow.createCell(i); cell.setCellType(Cell.CELL_TYPE_STRING); cell.setCellValue(metadata.get(i).getDescriptiveName()); } for (int i = 0; i < result.getResult().size(); i++) { List<Object> objects = result.getResult().get(i); Row row = sheet.createRow(i + 1); for (int c = 0; c < metadata.size(); c++) { final FieldType type = metadata.get(c).getType(); final int excelType = getExcelType(type); Cell cell = row.createCell(c, excelType); writeExcelValue(cell, objects.get(c), type); } } try { final ByteArrayOutputStream output = new ByteArrayOutputStream(); workbook.write(output); output.close(); return output.toByteArray(); } catch (IOException e) { throw Throwables.propagate(e); } }); } private static void writeExcelValue(Cell cell, Object field, FieldType type) { if (field == null) { return; } switch (type) { case STRING: case TIMESTAMP: case TIME: cell.setCellValue(field.toString()); break; case LONG: case INTEGER: case DOUBLE: cell.setCellValue(((Number) field).doubleValue()); break; default: throw new IllegalArgumentException("field type is not supported"); } } private static int getExcelType(FieldType type) { switch (type) { case STRING: return Cell.CELL_TYPE_STRING; case LONG: case INTEGER: case DOUBLE: return Cell.CELL_TYPE_NUMERIC; case BOOLEAN: return Cell.CELL_TYPE_BOOLEAN; default: return Cell.CELL_TYPE_BLANK; } } }