diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml index 5e6e3e7..4e3c5c4 100644 --- a/docker/docker-compose.yml +++ b/docker/docker-compose.yml @@ -77,6 +77,15 @@ services: processor-sklearn: image: miguelfc/marble-processor-sklearn + ports: + - "8080" + depends_on: + - registry + links: + - registry + + plotter-dous: + image: miguelfc/marble-plotter-dous ports: - "8080" depends_on: diff --git a/marble-core/pom.xml b/marble-core/pom.xml index 01cdce7..d3bdee5 100644 --- a/marble-core/pom.xml +++ b/marble-core/pom.xml @@ -5,7 +5,7 @@ org.marble marble-core - 1.0.4-RELEASE + 1.1.0-RELEASE 1.8 @@ -51,11 +51,6 @@ - - org.marble - marble-model - 1.0.1-RELEASE - org.springframework.boot diff --git a/marble-core/src/main/docker/README.md b/marble-core/src/main/docker/README.md index 762070c..eb1d278 100644 --- a/marble-core/src/main/docker/README.md +++ b/marble-core/src/main/docker/README.md @@ -1,6 +1,7 @@ # Supported tags and respective `Dockerfile` links -- [`1.0.4-RELEASE`, `latest`] +- [`1.1.0-RELEASE`, `latest`] +- [`1.0.4-RELEASE`] - [`1.0.3-RELEASE`] - [`1.0.2-RELEASE`] - [`1.0.1-RELEASE`] diff --git a/marble-core/src/main/java/org/marble/commons/config/SecurityConfig.java b/marble-core/src/main/java/org/marble/commons/config/SecurityConfig.java index 688eb47..8323c65 100644 --- a/marble-core/src/main/java/org/marble/commons/config/SecurityConfig.java +++ b/marble-core/src/main/java/org/marble/commons/config/SecurityConfig.java @@ -70,7 +70,8 @@ protected void configure(HttpSecurity http) throws Exception { "/api/jobs/**", "/api/charts/**", "/api/posts/**", - "/api/processedPosts/**") + "/api/processedPosts/**", + "/api/mongo_file/**") .hasRole(ROLE_GUEST) .antMatchers("/api/topics/**", "/api/jobs/**", diff --git a/marble-core/src/main/java/org/marble/commons/domain/projections/ChartExtendedProjection.java b/marble-core/src/main/java/org/marble/commons/domain/projections/ChartExtendedProjection.java index faffb87..228a575 100644 --- a/marble-core/src/main/java/org/marble/commons/domain/projections/ChartExtendedProjection.java +++ b/marble-core/src/main/java/org/marble/commons/domain/projections/ChartExtendedProjection.java @@ -1,6 +1,7 @@ package org.marble.commons.domain.projections; import java.math.BigInteger; +import java.util.ArrayList; import java.util.Date; import org.marble.model.domain.model.Chart; @@ -19,6 +20,7 @@ public interface ChartExtendedProjection { String getName(); String getDescription(); String getType(); + String getCustomType(); @Value("#{target.topic != null? target.topic.name : null}") String getTopicName(); @@ -28,6 +30,7 @@ public interface ChartExtendedProjection { BasicDBObject getOptions(); BasicDBObject getData(); + ArrayList getFigures(); Date getCreatedAt(); } diff --git a/marble-core/src/main/java/org/marble/commons/domain/repository/MongoFileRepository.java b/marble-core/src/main/java/org/marble/commons/domain/repository/MongoFileRepository.java new file mode 100644 index 0000000..8eb8979 --- /dev/null +++ b/marble-core/src/main/java/org/marble/commons/domain/repository/MongoFileRepository.java @@ -0,0 +1,107 @@ +package org.marble.commons.domain.repository; + +import static org.springframework.data.mongodb.core.query.Criteria.where; +import static org.springframework.data.mongodb.core.query.Query.query; + +import java.util.List; + +import org.marble.model.domain.model.MongoFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.domain.Sort; +import org.springframework.data.domain.Sort.Direction; +import org.springframework.data.mongodb.core.MongoOperations; +import org.springframework.data.mongodb.core.convert.MongoConverter; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.data.mongodb.gridfs.GridFsOperations; +import org.springframework.stereotype.Repository; +import org.springframework.util.StringUtils; + +import com.mongodb.DBObject; +import com.mongodb.gridfs.GridFSDBFile; +import com.mongodb.gridfs.GridFSFile; + +@Repository +public class MongoFileRepository { + + Logger log = LoggerFactory.getLogger(getClass()); + + private final GridFsOperations gridFs; + + private final MongoOperations mongo; + + private final MongoConverter mongoConverter; + + @Autowired + public MongoFileRepository(GridFsOperations gridFs, MongoOperations mongo, + MongoConverter mongoConverter) { + this.gridFs = gridFs; + this.mongo = mongo; + this.mongoConverter = mongoConverter; + } + + public MongoFile findById(String id) { + return findBy("_id", id, DBObject.class); + } + + public MongoFile findByFilename(String filename) { + return findByFilename(filename, DBObject.class); + } + + public MongoFile findByFilename(String filename, Class metadataType) { + return findBy("filename", filename, metadataType); + } + + @SuppressWarnings({"unchecked"}) + public String[] listFilenames(String locationPattern, Criteria... additionalCriteria) { + if (!StringUtils.hasText(locationPattern)) { + return new String[0]; + } + + Criteria criteria = null; + + criteria = where("filename").is(locationPattern); + if (additionalCriteria != null && additionalCriteria.length != 0) { + criteria = criteria.andOperator(additionalCriteria); + } + Query query = query(criteria); + List filenames = + mongo.getCollection("fs.files").distinct("filename", query.getQueryObject()); + return filenames.toArray(new String[0]); + } + + public GridFSFile insert(MongoFile> file) { + Object metadata = file.getMetadata(); + DBObject metadataDbObject = (DBObject) mongoConverter.convertToMongoType(metadata); + return gridFs.store(file.getContent(), file.getFilename(), file.getContentType(), metadataDbObject); + } + + public void delete(Query query) { + gridFs.delete(query); + } + + protected MongoFile findBy(String property, String value, Class metadataType) { + Query query = query(where(property).is(value)).with(new Sort(Direction.ASC, "uploadDate")); + log.info("Query: {}", query.getQueryObject()); + GridFSDBFile gridFsFile = gridFs.findOne(query); + return createMongoFile(gridFsFile, metadataType); + } + + @SuppressWarnings("unchecked") + protected MongoFile createMongoFile(GridFSDBFile gridFsFile, Class metadataType) { + if (gridFsFile == null) { + return null; + } + T metadata = null; + if (DBObject.class.equals(metadataType)) { + metadata = (T) gridFsFile.getMetaData(); + } else { + metadata = mongoConverter.read(metadataType, gridFsFile.getMetaData()); + } + return MongoFile.builder().content(gridFsFile.getInputStream()).id(gridFsFile.getId()) + .contentType(gridFsFile.getContentType()).uploadDate(gridFsFile.getUploadDate()) + .filename(gridFsFile.getFilename()).metadata(metadata).md5(gridFsFile.getMD5()).build(); + } +} diff --git a/marble-core/src/main/java/org/marble/commons/executor/plotter/PlotterExecutorImpl.java b/marble-core/src/main/java/org/marble/commons/executor/plotter/PlotterExecutorImpl.java index 5944ef7..21c1d93 100644 --- a/marble-core/src/main/java/org/marble/commons/executor/plotter/PlotterExecutorImpl.java +++ b/marble-core/src/main/java/org/marble/commons/executor/plotter/PlotterExecutorImpl.java @@ -1,6 +1,8 @@ package org.marble.commons.executor.plotter; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import java.math.BigInteger; import java.util.ArrayList; import java.util.List; @@ -11,10 +13,12 @@ import org.marble.commons.exception.InvalidExecutionException; import org.marble.commons.service.DatastoreService; import org.marble.commons.service.JobService; +import org.marble.commons.service.MongoFileService; import org.marble.commons.service.ChartService; import org.marble.commons.service.ProcessedPostService; import org.marble.commons.service.TopicService; import org.marble.model.domain.model.Job; +import org.marble.model.domain.model.MongoFile; import org.marble.model.domain.model.Topic; import org.marble.model.domain.model.Chart; import org.marble.model.model.JobParameters; @@ -22,9 +26,13 @@ import org.marble.model.model.PlotterInput; import org.marble.model.model.PlotterOutput; +import com.google.common.io.BaseEncoding; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonSyntaxException; +import com.mongodb.BasicDBObject; +import com.mongodb.DBObject; +import com.mongodb.gridfs.GridFSFile; import com.netflix.appinfo.InstanceInfo; import com.netflix.discovery.EurekaClient; @@ -38,209 +46,234 @@ @Scope("prototype") public class PlotterExecutorImpl implements PlotterExecutor { - private static final Logger log = LoggerFactory.getLogger(PlotterExecutorImpl.class); + private static final Logger log = LoggerFactory.getLogger(PlotterExecutorImpl.class); - public static final String id = PlotterExecutorImpl.class.getSimpleName(); + public static final String id = PlotterExecutorImpl.class.getSimpleName(); - public static final String label = "Executing"; + public static final String label = "Executing"; - @Autowired - JobService executionService; + @Autowired + JobService executionService; - @Autowired - TopicService topicService; + @Autowired + TopicService topicService; - @Autowired - ChartService plotService; - - @Autowired - DatastoreService datastoreService; + @Autowired + ChartService plotService; - @Autowired - private EurekaClient discoveryClient; + @Autowired + DatastoreService datastoreService; - private Job execution; + @Autowired + MongoFileService mongoFileService; - @Override - public void setJob(Job execution) { - this.execution = execution; - } + @Autowired + private EurekaClient discoveryClient; - @Override - public void run() { - String msg = ""; - try { - log.info("Initializing execution..."); - Thread.sleep(1000); - } catch (InterruptedException e) { - } + private Job execution; - try { + @Override + public void setJob(Job execution) { + this.execution = execution; + } - BigInteger id = execution.getId(); + @Override + public void run() { + String msg = ""; + try { + log.info("Initializing execution..."); + Thread.sleep(1000); + } catch (InterruptedException e) { + } - logMsg("Starting plotter <" + id + ">.", "info", null); + try { - // Changing execution state - execution.setStatus(JobStatus.Running); - execution = executionService.save(execution); + BigInteger id = execution.getId(); - if (execution.getTopic() != null) { - plot(); - } + logMsg("Starting plotter <" + id + ">.", "info", null); - } catch (Exception e) { - logMsg("An error ocurred while plotting posts with execution <" + execution.getId() + ">. Execution aborted.", "error", e); - execution.setStatus(JobStatus.Aborted); - try { - execution = executionService.save(execution); - } catch (InvalidExecutionException e1) { - log.error("Post couldn't be refreshed on the execution object."); - } + // Changing execution state + execution.setStatus(JobStatus.Running); + execution = executionService.save(execution); - return; - } + if (execution.getTopic() != null) { + plot(); + } + + } catch (Exception e) { + logMsg("An error ocurred while plotting posts with execution <" + execution.getId() + + ">. Execution aborted.", "error", e); + execution.setStatus(JobStatus.Aborted); + try { + execution = executionService.save(execution); + } catch (InvalidExecutionException e1) { + log.error("Post couldn't be refreshed on the execution object."); + } + + return; } + } + + private void plot() throws InvalidExecutionException { + + // Get the associated topic + Topic topic = execution.getTopic(); - private void plot() throws InvalidExecutionException { + String msg; - // Get the associated topic - Topic topic = execution.getTopic(); + Gson gson = new GsonBuilder().create(); - String msg; + List chartsList = new ArrayList<>(); - Gson gson = new GsonBuilder().create(); + // Loop for each processing stage + for (JobParameters parameter : this.execution.getParameters()) { - List chartsList = new ArrayList<>(); - - // Loop for each processing stage - for (JobParameters parameter : this.execution.getParameters()) { + String processorName = parameter.getName(); + logMsg("Starting plotter <" + processorName + ">", "info", null); - String processorName = parameter.getName(); - logMsg("Starting plotter <" + processorName + ">", "info", null); - - InstanceInfo hostInfo = null; + InstanceInfo hostInfo = null; + try { + hostInfo = discoveryClient.getNextServerFromEureka(processorName, Boolean.FALSE); + } catch (Exception e) { + logMsg("Service <" + processorName + "> doesn't exists. Skipping.", "warn", null); + continue; + } + String serviceUrl = hostInfo.getHomePageUrl() + "api/plot"; + log.error("Home page: " + serviceUrl); + + AsyncHttpClient asyncHttpClient = new DefaultAsyncHttpClient(); + + PlotterInput input = new PlotterInput(); + input.setTopicName(topic.getName()); + input.setOptions(parameter.getOptions()); + + + + final String url = new String(serviceUrl); + CompletableFuture promise = asyncHttpClient.preparePost(url) + .setBody(gson.toJson(input)).setHeader("Content-Type", "application/json").execute() + .toCompletableFuture().thenApply(resp -> { try { - hostInfo = discoveryClient.getNextServerFromEureka(processorName, Boolean.FALSE); + PlotterOutput output = null; + output = gson.fromJson(resp.getResponseBody(), PlotterOutput.class); + if (output != null) { + List temporaryChartsList = output.getCharts(); + for (Chart chart : temporaryChartsList) { + chart.setJobId(execution.getId()); + chart.setTopic(topic); + + try { + chart = plotService.save(chart); + } catch (Exception e) { + logMsg("Chart for topic <" + topic.getName() + "> couldn't be saved.", "error", + e); + } + + // By convention, the figures list coming from the plotter contains the figures in + // base64. + // This will be converted to bytes and saved in gridfs, and the ids will be saved + // in the same list. + if (chart.getFigures() != null) { + ArrayList figureReferences = new ArrayList<>(); + for (String figure : chart.getFigures()) { + byte[] binaryFigure = BaseEncoding.base64().decode(figure); + InputStream in = new ByteArrayInputStream(binaryFigure); + MongoFile mongoFile = + new MongoFile(chart.getId() + ".png", in, "image/png", new BasicDBObject()); + GridFSFile insertedFile = mongoFileService.insert(mongoFile); + figureReferences.add(insertedFile.getId().toString()); + } + chart.setFigures(figureReferences); + } + + try { + chart = plotService.save(chart); + } catch (Exception e) { + logMsg("Chart for topic <" + topic.getName() + "> couldn't be saved.", "error", + e); + } + + chartsList.add(chart); + } + // TODO Create plot object + } + } catch (JsonSyntaxException e) { + // TODO Auto-generated catch block + logMsg("Topic <" + topic.getName() + "> couldn't be plotted.", "error", e); } - catch (Exception e) { - logMsg("Service <"+processorName+"> doesn't exists. Skipping.", "warn", null); - } - String serviceUrl = hostInfo.getHomePageUrl() + "api/plot"; - log.error("Home page: " + serviceUrl); - - AsyncHttpClient asyncHttpClient = new DefaultAsyncHttpClient(); - - PlotterInput input = new PlotterInput(); - input.setTopicName(topic.getName()); - input.setOptions(parameter.getOptions()); - - - - final String url = new String(serviceUrl); - CompletableFuture promise = asyncHttpClient - .preparePost(url) - .setBody(gson.toJson(input)) - .setHeader("Content-Type", "application/json") - .execute() - .toCompletableFuture() - .thenApply(resp -> { - try { - log.error("AQui voy: " + resp.getResponseBody()); - PlotterOutput output = null; - output = gson.fromJson(resp.getResponseBody(), PlotterOutput.class); - if (output != null) { - List temporaryChartsList = output.getCharts(); - for (Chart chart : temporaryChartsList) { - chart.setJobId(execution.getId()); - chart.setTopic(topic); - try { - plotService.save(chart); - } catch (Exception e) { - logMsg("Chart for topic <" + topic.getName() + "> couldn't be saved.", "error", e); - } - chartsList.add(chart); - } - // TODO Create plot object - } - } catch (JsonSyntaxException e) { - // TODO Auto-generated catch block - logMsg("Topic <" + topic.getName() + "> couldn't be plotted.", "error", e); - } - try { - asyncHttpClient.close(); - } catch (IOException e) { - logMsg("An error occurred while closing asyncHttpClient .", "error", e); - } - return resp; - }) - .exceptionally((t) -> {logMsg("An error occurred while using plotter.", "error", t);return null;}); - - // Force the operation to be synchronous - promise.join(); - - /* try { - asyncHttpClient.close(); + asyncHttpClient.close(); } catch (IOException e) { - logMsg("An error occurred while closing asyncHttpClient.", "error", e); + logMsg("An error occurred while closing asyncHttpClient .", "error", e); } - */ + return resp; + }).exceptionally((t) -> { + logMsg("An error occurred while using plotter.", "error", t); + return null; + }); - } - msg = "The plotter operation for topic <" + topic.getName() + "> has finished."; - log.info(msg); - execution.appendLog(msg); - // TODO - execution.setCharts(chartsList); - execution.setStatus(JobStatus.Stopped); + // Force the operation to be synchronous + promise.join(); - execution = executionService.save(execution); - } + /* + * try { asyncHttpClient.close(); } catch (IOException e) { + * logMsg("An error occurred while closing asyncHttpClient.", "error", e); } + */ - public void logMsg(String message, String level, Throwable exception) { - if (level != null) { - switch (level) { - case "error": - if (exception == null) - log.error(message); - else - log.error(message, exception); - break; - case "warn": - if (exception == null) - log.warn(message); - else - log.warn(message, exception); - break; - case "debug": - if (exception == null) - log.debug(message); - else - log.debug(message, exception); - break; - case "trace": - if (exception == null) - log.trace(message); - else - log.trace(message, exception); - break; - case "info": - default: - if (exception == null) - log.info(message); - else - log.info(message, exception); - break; - } - } else { - if (exception == null) - log.info(message); - else - log.info(message, exception); - } - execution.appendLog(message); } + msg = "The plotter operation for topic <" + topic.getName() + "> has finished."; + log.info(msg); + execution.appendLog(msg); + // TODO + execution.setCharts(chartsList); + execution.setStatus(JobStatus.Stopped); + + execution = executionService.save(execution); + } + + public void logMsg(String message, String level, Throwable exception) { + if (level != null) { + switch (level) { + case "error": + if (exception == null) + log.error(message); + else + log.error(message, exception); + break; + case "warn": + if (exception == null) + log.warn(message); + else + log.warn(message, exception); + break; + case "debug": + if (exception == null) + log.debug(message); + else + log.debug(message, exception); + break; + case "trace": + if (exception == null) + log.trace(message); + else + log.trace(message, exception); + break; + case "info": + default: + if (exception == null) + log.info(message); + else + log.info(message, exception); + break; + } + } else { + if (exception == null) + log.info(message); + else + log.info(message, exception); + } + execution.appendLog(message); + } -} \ No newline at end of file +} diff --git a/marble-core/src/main/java/org/marble/commons/service/MongoFileService.java b/marble-core/src/main/java/org/marble/commons/service/MongoFileService.java new file mode 100644 index 0000000..c257182 --- /dev/null +++ b/marble-core/src/main/java/org/marble/commons/service/MongoFileService.java @@ -0,0 +1,24 @@ +package org.marble.commons.service; + +import org.marble.model.domain.model.MongoFile; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; + +import com.mongodb.DBObject; +import com.mongodb.gridfs.GridFSFile; + +public interface MongoFileService { + + public MongoFile findById(String id); + + public MongoFile findByFilename(String filename); + + public MongoFile findByFilename(String filename, Class metadataType); + + public String[] listFilenames(String locationPattern, Criteria... additionalCriteria); + + public GridFSFile insert(MongoFile> file); + + public void delete(Query query); + +} diff --git a/marble-core/src/main/java/org/marble/commons/service/MongoFileServiceImpl.java b/marble-core/src/main/java/org/marble/commons/service/MongoFileServiceImpl.java new file mode 100644 index 0000000..8373e63 --- /dev/null +++ b/marble-core/src/main/java/org/marble/commons/service/MongoFileServiceImpl.java @@ -0,0 +1,49 @@ +package org.marble.commons.service; + +import org.marble.commons.domain.repository.MongoFileRepository; +import org.marble.model.domain.model.MongoFile; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.mongodb.core.query.Criteria; +import org.springframework.data.mongodb.core.query.Query; +import org.springframework.stereotype.Service; + +import com.mongodb.DBObject; +import com.mongodb.gridfs.GridFS; +import com.mongodb.gridfs.GridFSFile; + +@Service +public class MongoFileServiceImpl implements MongoFileService { + + @Autowired + MongoFileRepository mongoFileRepository; + + @Override + public MongoFile findById(String id) { + return mongoFileRepository.findById(id); + } + + @Override + public MongoFile findByFilename(String filename) { + return mongoFileRepository.findByFilename(filename); + } + + @Override + public MongoFile findByFilename(String filename, Class metadataType) { + return mongoFileRepository.findByFilename(filename, metadataType); + } + + @Override + public String[] listFilenames(String locationPattern, Criteria... additionalCriteria) { + return mongoFileRepository.listFilenames(locationPattern, additionalCriteria); + } + + @Override + public GridFSFile insert(MongoFile> file) { + return mongoFileRepository.insert(file); + } + + @Override + public void delete(Query query) { + mongoFileRepository.delete(query); + } +} diff --git a/marble-core/src/main/java/org/marble/commons/service/ResetServiceImpl.java b/marble-core/src/main/java/org/marble/commons/service/ResetServiceImpl.java index a667fa7..fbf8e22 100644 --- a/marble-core/src/main/java/org/marble/commons/service/ResetServiceImpl.java +++ b/marble-core/src/main/java/org/marble/commons/service/ResetServiceImpl.java @@ -149,7 +149,7 @@ public void getTheSpecial() { mainPlotData.add(itemData); } - plot.setData(mainPlotData); + //plot.setData(mainPlotData); Map mainOptions = new HashMap<>(); diff --git a/marble-core/src/main/java/org/marble/commons/web/MongoFileRestController.java b/marble-core/src/main/java/org/marble/commons/web/MongoFileRestController.java new file mode 100644 index 0000000..bbcbc33 --- /dev/null +++ b/marble-core/src/main/java/org/marble/commons/web/MongoFileRestController.java @@ -0,0 +1,82 @@ +package org.marble.commons.web; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.PrintWriter; + +import javax.servlet.http.HttpServletResponse; + +import org.apache.commons.io.IOUtils; +import org.marble.commons.model.HomeInformation; +import org.marble.commons.service.InformationService; +import org.marble.commons.service.JobService; +import org.marble.commons.service.MongoFileService; +import org.marble.model.domain.model.MongoFile; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RestController; + +import com.mongodb.DBObject; + +@RestController +public class MongoFileRestController { + @SuppressWarnings("unused") + private static final Logger log = LoggerFactory.getLogger(MongoFileRestController.class); + + @Autowired + MongoFileService mongoFileService; + + + @RequestMapping(value = "/api/mongo_file/{id}", method = RequestMethod.GET) + public void getHomeInfo(@PathVariable(value = "id") String id, HttpServletResponse response) throws IOException { + MongoFile file = mongoFileService.findById(id); + response.setHeader("Content-Disposition", "inline"); + response.setHeader("Content-Type", file.getContentType()); + + OutputStream output = response.getOutputStream(); + InputStream input = file.getContent(); + byte[] buffer = new byte[10240]; + for (int length = 0; (length = input.read(buffer)) > 0;) { + output.write(buffer, 0, length); + } + output.close(); + } + + /* + @RequestMapping(value = "/api/info/validation/process", method = RequestMethod.POST) + public @ResponseBody ResponseEntity validateProcessInfo(@RequestBody(required = true) JobRestRequest jobRestRequest) { + BigInteger executionId = null; + try { + JobModuleParameters jobModuleParameters = new JobModuleParameters(); + + // TODO Check getProcessorModules, it seems broken + List modules = moduleService.getProcessorModules(); + for (JobModuleDefinition module : modules) { + log.error(module.getName()); + } + + // TODO Validate the module name + if (jobRestRequest != null && jobRestRequest.getModule() != null) { + jobModuleParameters.setModule("org.marble.commons.executor.processor." + jobRestRequest.getModule()); + jobModuleParameters.setOperation(jobRestRequest.getOperation()); + } + + if (jobRestRequest.getParameters() != null) { + jobModuleParameters.setParameters(jobRestRequest.getParameters()); + } + executionId = jobService.executeProcessor(null, jobModuleParameters); + JobRestResult executionResult = new JobRestResult(executionId); + executionResult.setMessage("Execution started."); + return new ResponseEntity(executionResult, HttpStatus.OK); + } catch (Exception e) { + JobRestResult executionResult = new JobRestResult(e.getMessage()); + return new ResponseEntity(executionResult, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + */ +} \ No newline at end of file diff --git a/marble-core/src/main/resources/web/js/controllers/chart/chartViewController.js b/marble-core/src/main/resources/web/js/controllers/chart/chartViewController.js index 37dfa33..0c00ec6 100644 --- a/marble-core/src/main/resources/web/js/controllers/chart/chartViewController.js +++ b/marble-core/src/main/resources/web/js/controllers/chart/chartViewController.js @@ -33,14 +33,17 @@ angular.module('marbleCoreApp').controller( $scope.updateDate = new Date(); // Google Chart - $scope.chartObject = {}; - - $scope.chartObject.type = $scope.chart.type; - - $scope.chartObject.data = $scope.chart.data; - - $scope.chartObject.options = $scope.chart.options; - + if ($scope.chart.type == "Google Chart") { + $scope.googleChartObject = {}; + $scope.googleChartObject.type = $scope.chart.customType; + $scope.googleChartObject.data = $scope.chart.data; + $scope.googleChartObject.options = $scope.chart.options; + } + // Figure List + else if ($scope.chart.type == "Figure List") { + $scope.figureListObject = {}; + $scope.figureListObject.figures = $scope.chart.figures; + } }, function(error) { // TODO Handle Error 404 }); diff --git a/marble-core/src/main/resources/web/sass/styles.sass b/marble-core/src/main/resources/web/sass/styles.sass index 7f1c093..ca0001d 100644 --- a/marble-core/src/main/resources/web/sass/styles.sass +++ b/marble-core/src/main/resources/web/sass/styles.sass @@ -570,4 +570,9 @@ a width: 100% .help-icon - float: right \ No newline at end of file + float: right + +.chart-figures-container + text-align: center + .chart-figure + max-width: 100% \ No newline at end of file diff --git a/marble-core/src/main/resources/web/templates/views/chart/view.html b/marble-core/src/main/resources/web/templates/views/chart/view.html index cc62360..e839f70 100644 --- a/marble-core/src/main/resources/web/templates/views/chart/view.html +++ b/marble-core/src/main/resources/web/templates/views/chart/view.html @@ -1,55 +1,62 @@ - - - Details for Plot {{chart.name}} - - - + + + Details for Plot + {{chart.name}} + + + - - - - - Description - - - {{chart.description? chart.description : 'This chart does not have a description defined.'}} - - - Go back - to Topic - Go back - to Job - - - - - - - - - - - + + + + + Description + + + {{chart.description? chart.description : 'This chart does + not have a description defined.'}} + + + + Go back to Topic Go back to Job + + + + + + + + + + + - - - - Plot - + + + + Plot + + + + + + + + + + + + + - - - - - - - - - - - + diff --git a/marble-model/pom.xml b/marble-model/pom.xml index 9ca559c..7d161fd 100644 --- a/marble-model/pom.xml +++ b/marble-model/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.marble marble-model - 1.0.1-RELEASE + 1.0.2-RELEASE jar diff --git a/marble-model/src/main/java/org/marble/model/domain/model/Chart.java b/marble-model/src/main/java/org/marble/model/domain/model/Chart.java index 1fe2a21..067be20 100644 --- a/marble-model/src/main/java/org/marble/model/domain/model/Chart.java +++ b/marble-model/src/main/java/org/marble/model/domain/model/Chart.java @@ -2,6 +2,7 @@ import java.io.Serializable; import java.math.BigInteger; +import java.util.ArrayList; import java.util.Date; import java.util.List; import java.util.Map; @@ -26,6 +27,8 @@ public class Chart implements Serializable { private static final long serialVersionUID = 6936532299491147949L; + public static final String TYPE_GOOGLE_CHART = "Google Chart"; + @Id @JsonSerialize(using = BigIntegerSerializer.class) private BigInteger id; @@ -35,9 +38,11 @@ public class Chart implements Serializable { private String name; private String description; - + private String type; + private String customType; + @DBRef private Topic topic; @@ -47,6 +52,8 @@ public class Chart implements Serializable { private BasicDBObject options; private BasicDBObject data; + + private ArrayList figures; @CreatedDate public Date createdAt; @@ -76,11 +83,19 @@ public void setDescription(String description) { } public String getType() { - return type; + return type; } public void setType(String type) { - this.type = type; + this.type = type; + } + + public String getCustomType() { + return customType; + } + + public void setCustomType(String type) { + this.customType = type; } public Topic getTopic() { @@ -104,15 +119,18 @@ public BasicDBObject getData() { return data; } - @Deprecated - public void setData(List> data) { - this.data = null; - } - public void setData(Map data) { this.data = new BasicDBObject(data); } + public ArrayList getFigures() { + return figures; + } + + public void setFigures(ArrayList figures) { + this.figures = figures; + } + public Date getCreatedAt() { return createdAt; } diff --git a/marble-model/src/main/java/org/marble/model/domain/model/MongoFile.java b/marble-model/src/main/java/org/marble/model/domain/model/MongoFile.java new file mode 100644 index 0000000..5f099fc --- /dev/null +++ b/marble-model/src/main/java/org/marble/model/domain/model/MongoFile.java @@ -0,0 +1,141 @@ +package org.marble.model.domain.model; + +import java.io.InputStream; +import java.util.Date; + +import org.springframework.data.annotation.Transient; +import org.springframework.data.mongodb.core.mapping.Document; +import org.springframework.util.Assert; + +@Document(collection = "fs.files") +public class MongoFile { + + private Object id; + + private final String filename; + + @Transient + private final InputStream content; + + private final T metadata; + + private String contentType; + + private Date uploadDate; + + // this checksum is created by GridFs + private String md5; + + private MongoFile(Builder builder) { + this.id = builder.id; + this.filename = builder.filename; + this.content = builder.content; + this.metadata = builder.metadata; + this.contentType = builder.contentType; + this.uploadDate = builder.uploadDate; + this.md5 = builder.md5; + } + + public MongoFile(String filename, InputStream content, String contentType, T metadata) { + this.filename = filename; + this.content = content; + this.contentType = contentType; + this.metadata = metadata; + } + + public String getFilename() { + return filename; + } + + public InputStream getContent() { + return content; + } + + public Object getId() { + return id; + } + + public String getContentType() { + return contentType; + } + + public Date getUploadDate() { + return uploadDate; + } + + public String getMd5() { + return md5; + } + + public T getMetadata() { + return metadata; + } + + public boolean isStored() { + return md5 != null; + } + + @Override + public String toString() { + return "MongoFile [id=" + id + "]"; + } + + public static Builder builder() { + return new Builder(); + } + + public static class Builder { + private Object id; + private String filename; + private InputStream content; + private T metadata; + private String contentType; + private Date uploadDate; + private String md5; + + public Builder id(Object id) { + this.id = id; + return this; + } + + public Builder filename(String filename) { + this.filename = filename; + return this; + } + + public Builder content(InputStream content) { + this.content = content; + return this; + } + + public Builder metadata(T metadata) { + this.metadata = metadata; + return this; + } + + public Builder contentType(String contentType) { + this.contentType = contentType; + return this; + } + + public Builder uploadDate(Date uploadDate) { + this.uploadDate = uploadDate; + return this; + } + + public Builder md5(String md5) { + this.md5 = md5; + return this; + } + + public MongoFile build() { + Assert.notNull(id); + Assert.notNull(filename); + Assert.notNull(content); + Assert.notNull(uploadDate); + Assert.notNull(md5); + return new MongoFile(this); + } + } + +} \ No newline at end of file diff --git a/marble-plotter-dous/.gitignore b/marble-plotter-dous/.gitignore new file mode 100644 index 0000000..9143edd --- /dev/null +++ b/marble-plotter-dous/.gitignore @@ -0,0 +1,110 @@ + +# Created by https://www.gitignore.io/api/maven,python + +### Maven ### +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties + +# Exclude maven wrapper +!/.mvn/wrapper/maven-wrapper.jar + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*,cover +.hypothesis/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder + +# Jupyter Notebook +.ipynb_checkpoints + +# pyenv +.python-version + +# celery beat schedule file +celerybeat-schedule + +# dotenv +.env + +# virtualenv +.venv +venv/ +ENV/ + +# Spyder project settings +.spyderproject + +# Rope project settings +.ropeproject + +# End of https://www.gitignore.io/api/maven,python \ No newline at end of file diff --git a/marble-plotter-dous/pom.xml b/marble-plotter-dous/pom.xml new file mode 100644 index 0000000..e02e2da --- /dev/null +++ b/marble-plotter-dous/pom.xml @@ -0,0 +1,49 @@ + + 4.0.0 + org.marble + marble-plotter-dous + 1.0.0-RELEASE + + 1.8 + + + org.marble + marble + 1.0.0-RELEASE + ../pom.xml + + + + + com.spotify + docker-maven-plugin + ${docker.plugin.version} + + + package + + build + + + + + ${docker.image.prefix}/${project.artifactId} + + ${project.version} + latest + + ${project.basedir}/src/main/docker + + + / + ${project.basedir}/src/main/python + **/*.py + + + + + + + \ No newline at end of file diff --git a/marble-plotter-dous/src/main/docker/Dockerfile b/marble-plotter-dous/src/main/docker/Dockerfile new file mode 100644 index 0000000..808d423 --- /dev/null +++ b/marble-plotter-dous/src/main/docker/Dockerfile @@ -0,0 +1,3 @@ +FROM python:3.6.0-onbuild +EXPOSE 8080 +ENTRYPOINT ["python","server.py"] diff --git a/marble-plotter-dous/src/main/docker/README-short.txt b/marble-plotter-dous/src/main/docker/README-short.txt new file mode 100644 index 0000000..281f038 --- /dev/null +++ b/marble-plotter-dous/src/main/docker/README-short.txt @@ -0,0 +1 @@ +Processor module for Marble, an opinion mining platform, modular and open sourced. diff --git a/marble-plotter-dous/src/main/docker/README.md b/marble-plotter-dous/src/main/docker/README.md new file mode 100644 index 0000000..31427ee --- /dev/null +++ b/marble-plotter-dous/src/main/docker/README.md @@ -0,0 +1,39 @@ +# Supported tags and respective `Dockerfile` links + +- [`1.0.0-RELEASE`, `latest`] + +# What is Marble? + +[Marble](http://marble.miguelfc.com/) is an opinion mining platform, modular and open sourced. This is part of a PhD project, being developed by [Miguel Fernandes](http://miguelfernandes.com/) for the University of Vigo, under the supervision of Ana Fernández Vilas and Rebeca Díaz Redondo. + +Marble is composed of several modules. This one, marble-processor-dous, is a plotter module, providing charts and reports to the system. + +All the modules of the platform them described in the [Github project](https://github.com/miguelfc/marble). + +# How to use this image + +As there are some dependencies between modules (namely mongodb, registry and core), it is recommended to use docker-compose to launch the required services in a coordinated way. There is a [docker-compose.yml](https://github.com/miguelfc/marble/blob/master/docker/docker-compose.yml) file provided as a baseline for you to use. + +The required services are mongodb, registry and core, so you can launch them using the up subcommand: + +```console +$ docker-compose up mongodb registry core +``` + +If you want to launch all the provided modules, then you can just use the up subcommand without arguments: + +```console +$ docker-compose up +``` + +Then, access it via `http://localhost:8080` or `http://host-ip:8080` in a browser. You can also access the registry via `http://localhost:11111` or `http://host-ip:1111` in a browser. + +For more specific details please check the [Github project](https://github.com/miguelfc/marble). + +# Issues + +If you have any problems with or questions about this image, please contact us through a [Github issue](https://github.com/miguelfc/marble/issues/new) in the Github project. + +# Contributing + +You are invited to contribute new features, fixes, or updates, large or small. Before you start to code, we recommend discussing your plans through a [Github issue](https://github.com/miguelfc/marble/issues/new), especially for more ambitious contributions. This gives other contributors a chance to point you in the right direction, give you feedback on your design, and help you find out if someone else is working on the same thing. \ No newline at end of file diff --git a/marble-plotter-dous/src/main/docker/requirements.txt b/marble-plotter-dous/src/main/docker/requirements.txt new file mode 100644 index 0000000..0a6190f --- /dev/null +++ b/marble-plotter-dous/src/main/docker/requirements.txt @@ -0,0 +1,10 @@ +flask +dnspython +netifaces +future +nltk +twython +numpy +pymongo +matplotlib +pandas \ No newline at end of file diff --git a/marble-plotter-dous/src/main/python/eureka/__init__.py b/marble-plotter-dous/src/main/python/eureka/__init__.py new file mode 100644 index 0000000..43c4ab0 --- /dev/null +++ b/marble-plotter-dous/src/main/python/eureka/__init__.py @@ -0,0 +1 @@ +__version__ = "0.6.1" diff --git a/marble-plotter-dous/src/main/python/eureka/client.py b/marble-plotter-dous/src/main/python/eureka/client.py new file mode 100644 index 0000000..42acb47 --- /dev/null +++ b/marble-plotter-dous/src/main/python/eureka/client.py @@ -0,0 +1,270 @@ +import json +import random +from urllib.error import URLError +from urllib.parse import urljoin, quote_plus +from eureka import requests +from eureka import ec2metadata +import logging +import dns.resolver +from eureka.requests import EurekaHTTPException + + +logger = logging.getLogger('eureka.client') + + +class EurekaClientException(Exception): + pass + + +class EurekaRegistrationFailedException(EurekaClientException): + pass + + +class EurekaUpdateFailedException(EurekaClientException): + pass + + +class EurekaHeartbeatFailedException(EurekaClientException): + pass + +class EurekaGetFailedException(EurekaClientException): + pass + + +class EurekaClient(object): + def __init__(self, app_name, ip_address=None, eureka_url=None, eureka_domain_name=None, host_name=None, data_center="Amazon", + vip_address=None, secure_vip_address=None, port=None, secure_port=None, use_dns=True, region=None, + prefer_same_zone=True, context="eureka/v2", eureka_port=None, + health_check_url=None): + super(EurekaClient, self).__init__() + self.app_name = app_name + self.ip_address = ip_address + self.eureka_url = eureka_url + self.data_center = data_center + if not host_name and data_center == "Amazon": + self.host_name = ec2metadata.get("public-hostname") + else: + self.host_name = host_name + # Virtual host name by which the clients identifies this service + self.vip_address = vip_address + self.secure_vip_address = secure_vip_address + self.port = port + if (port != None): + self.composed_port = { + "$": port, + "@enabled": True + } + else: + self.composed_port = { + "$": 80, + "@enabled": False + } + self.secure_port = secure_port + if (secure_port != None): + self.composed_secure_port = { + "$": secure_port, + "@enabled": True + } + else: + self.composed_secure_port = { + "$": 443, + "@enabled": False + } + self.use_dns = use_dns + # Region where eureka is deployed - For AWS specify one of the AWS regions, for other datacenters specify a + # arbitrary string indicating the region. + self.region = region + # Prefer a eureka server in same zone or not + self.prefer_same_zone = prefer_same_zone + # Domain name, if using DNS + self.eureka_domain_name = eureka_domain_name + #if eureka runs on a port that is not 80, this will go into the urls to eureka + self.eureka_port = eureka_port + # Relative URL to eureka + self.context = context + self.health_check_url = health_check_url + self.eureka_urls = self.get_eureka_urls() + + self.lease_info = { + "renewalIntervalInSecs": 5, + "durationInSecs": 10, + "registrationTimestamp": 0, + "lastRenewalTimestamp": 0, + "evictionTimestamp": 0, + "serviceUpTimestamp": 0 + } + + def _get_txt_records_from_dns(self, domain): + records = dns.resolver.query(domain, 'TXT') + for record in records: + for string in record.strings: + yield string + + def _get_zone_urls_from_dns(self, domain): + for zone in self._get_txt_records_from_dns(domain): + yield zone + + def get_zones_from_dns(self): + return { + zone_url.split(".")[0]: list(self._get_zone_urls_from_dns("txt.%s" % zone_url)) for zone_url in list( + self._get_zone_urls_from_dns('txt.%s.%s' % (self.region, self.eureka_domain_name)) + ) + } + + def get_eureka_urls(self): + if self.eureka_url: + return [self.eureka_url] + elif self.use_dns: + zone_dns_map = self.get_zones_from_dns() + zones = list(zone_dns_map.keys()) + assert len(zones) > 0, "No availability zones found for, please add them explicitly" + if self.prefer_same_zone: + if self.get_instance_zone() in zones: + zones = [zones.pop(zones.index(self.get_instance_zone()))] + zones # Add our zone as the first element + else: + logger.warn("No match for the zone %s in the list of available zones %s" % ( + self.get_instance_zone(), zones) + ) + service_urls = [] + for zone in zones: + eureka_instances = zone_dns_map[zone] + random.shuffle(eureka_instances) # Shuffle order for load balancing + for eureka_instance in eureka_instances: + server_uri = "http://%s" % eureka_instance + if self.eureka_port: + server_uri += ":%s" % self.eureka_port + eureka_instance_url = urljoin(server_uri, self.context, "/") + if not eureka_instance_url.endswith("/"): + eureka_instance_url = "%s/" % eureka_instance_url + service_urls.append(eureka_instance_url) + primary_server = service_urls.pop(0) + random.shuffle(service_urls) + service_urls.insert(0, primary_server) + logger.info("This client will talk to the following serviceUrls in order: %s" % service_urls) + return service_urls + + def get_instance_zone(self): + if self.data_center == "Amazon": + return ec2metadata.get('availability-zone') + else: + raise NotImplementedError("%s does not implement DNS lookups" % self.data_center) + + def register(self, initial_status="STARTING"): + data_center_info = { + 'name': self.data_center, + '@class': 'com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo' + } + if self.data_center == "Amazon": + data_center_info['metadata'] = { + 'ami-launch-index': ec2metadata.get('ami-launch-index'), + 'local-hostname': ec2metadata.get('local-hostname'), + 'availability-zone': ec2metadata.get('availability-zone'), + 'instance-id': ec2metadata.get('instance-id'), + 'public-ipv4': ec2metadata.get('public-ipv4'), + 'public-hostname': ec2metadata.get('public-hostname'), + 'ami-manifest-path': ec2metadata.get('ami-manifest-path'), + 'local-ipv4': ec2metadata.get('local-ipv4'), + 'ami-id': ec2metadata.get('ami-id'), + 'instance-type': ec2metadata.get('instance-type'), + } + instance_data = { + 'instance': { + 'instanceId': self.host_name + ":" + self.app_name + ":" + str(self.port), + 'hostName': self.host_name, + 'app': self.app_name, + 'ipAddr': self.ip_address, + 'status': initial_status, + 'port': self.composed_port, + 'securePort': self.composed_secure_port, + 'dataCenterInfo': data_center_info, + "leaseInfo": self.lease_info, + "homePageUrl": "http://" + self.host_name + ":" + str(self.port) + "/", + "statusPageUrl": "http://" + self.host_name + ":" + str(self.port) + "/info", + "healthCheckUrl": "http://" + self.host_name + ":" + str(self.port) + "/health", + 'vipAddress': self.vip_address or '', + 'secureVipAddress': self.secure_vip_address or '' + } + } + success = False + last_e = None + for eureka_url in self.eureka_urls: + try: + data = json.dumps(instance_data).encode("utf-8") + r = requests.post(urljoin(eureka_url, "apps/%s" % self.app_name), data, + headers={'Content-Type': 'application/json'}) + r.raise_for_status() + success = True + break + except (EurekaHTTPException, URLError) as e: + last_e = e + if not success: + raise EurekaRegistrationFailedException("Did not receive correct reply from any instances, last error: " + str(last_e)) + + def update_status(self, new_status): + instance_id = self.host_name + ":" + self.app_name + ":" + str(self.port) + if self.data_center == "Amazon": + instance_id = ec2metadata.get('instance-id') + success = False + last_e = None + for eureka_url in self.eureka_urls: + try: + r = requests.put(urljoin(eureka_url, "apps/%s/%s/status?value=%s" % ( + self.app_name, + instance_id, + new_status + ))) + r.raise_for_status() + success = True + break + except (EurekaHTTPException, URLError) as e: + last_e = e + if not success: + raise EurekaUpdateFailedException("Did not receive correct reply from any instances, last error: " + str(e)) + + def heartbeat(self): + instance_id = self.host_name + ":" + self.app_name + ":" + str(self.port) + if self.data_center == "Amazon": + instance_id = ec2metadata.get('instance-id') + success = False + for eureka_url in self.eureka_urls: + try: + r = requests.put(urljoin(eureka_url, "apps/%s/%s" % (self.app_name, instance_id))) + r.raise_for_status() + success = True + break + except (EurekaHTTPException, URLError) as e: + pass + if not success: + raise EurekaHeartbeatFailedException("Did not receive correct reply from any instances") + + #a generic get request, since most of the get requests for discovery will take a similar form + def _get_from_any_instance(self, endpoint): + for eureka_url in self.eureka_urls: + try: + r = requests.get(urljoin(eureka_url, endpoint), headers={ + 'accept': 'application/json', + 'accept-encoding': 'gzip', + }) + r.raise_for_status() + return json.loads(r.content) + except (EurekaHTTPException, URLError) as e: + pass + raise EurekaGetFailedException("Failed to GET %s from all instances" % endpoint) + + def get_apps(self): + return self._get_from_any_instance("apps") + + def get_app(self, app_id): + return self._get_from_any_instance("apps/%s" % app_id) + + def get_vip(self, vip_address): + return self._get_from_any_instance("vips/%s" % vip_address) + + def get_svip(self, vip_address): + return self._get_from_any_instance("svips/%s" % vip_address) + + def get_instance(self, instance_id): + return self._get_from_any_instance("instances/%s" % instance_id) + + def get_app_instance(self, app_id, instance_id): + return self._get_from_any_instance("apps/%s/%s" % (app_id, instance_id)) diff --git a/marble-plotter-dous/src/main/python/eureka/ec2metadata.py b/marble-plotter-dous/src/main/python/eureka/ec2metadata.py new file mode 100644 index 0000000..9c58cdc --- /dev/null +++ b/marble-plotter-dous/src/main/python/eureka/ec2metadata.py @@ -0,0 +1,100 @@ +# Copyright (c) 2013 Alon Swartz +# +# This file is part of ec2metadata. +# +# ec2metadata is free software; you can redistribute it and/or modify it +# under the terms of the GNU General Public License as published by the +# Free Software Foundation; either version 3 of the License, or (at your +# option) any later version. +# +import time +import urllib.request, urllib.parse, urllib.error +import socket + +METAOPTS = ['ami-id', 'ami-launch-index', 'ami-manifest-path', + 'ancestor-ami-id', 'availability-zone', 'block-device-mapping', + 'instance-id', 'instance-type', 'local-hostname', 'local-ipv4', + 'kernel-id', 'product-codes', 'public-hostname', 'public-ipv4', + 'public-keys', 'ramdisk-id', 'reservation-id', 'security-groups', + 'user-data'] + +class Error(Exception): + pass + +class EC2Metadata: + """Class for querying metadata from EC2""" + + def __init__(self, addr='169.254.169.254', api='2008-02-01'): + self.addr = addr + self.api = api + + if not self._test_connectivity(self.addr, 80): + raise Error("could not establish connection to: %s" % self.addr) + + @staticmethod + def _test_connectivity(addr, port): + for i in range(6): + s = socket.socket() + try: + s.connect((addr, port)) + s.close() + return True + except socket.error as e: + time.sleep(1) + + return False + + def _get(self, uri): + url = 'http://%s/%s/%s' % (self.addr, self.api, uri) + value = urllib.request.urlopen(url).read() + if "404 - Not Found" in value: + return None + + return value + + def get(self, metaopt): + """return value of metaopt""" + + if metaopt not in METAOPTS: + raise Error('unknown metaopt', metaopt, METAOPTS) + + if metaopt == 'availability-zone': + return self._get('meta-data/placement/availability-zone') + + if metaopt == 'public-keys': + public_keys = [] + data = self._get('meta-data/public-keys') + if not data: + return public_keys + + keyids = [ line.split('=')[0] for line in data.splitlines() ] + for keyid in keyids: + uri = 'meta-data/public-keys/%d/openssh-key' % int(keyid) + public_keys.append(self._get(uri).rstrip()) + + return public_keys + + if metaopt == 'user-data': + return self._get('user-data') + + return self._get('meta-data/' + metaopt) + +def get(metaopt): + """primitive: return value of metaopt""" + + m = EC2Metadata() + return m.get(metaopt) + +def display(metaopts, prefix=False): + """primitive: display metaopts (list) values with optional prefix""" + + m = EC2Metadata() + for metaopt in metaopts: + value = m.get(metaopt) + if not value: + value = "unavailable" + + if prefix: + print("%s: %s" % (metaopt, value)) + else: + print(value) diff --git a/marble-plotter-dous/src/main/python/eureka/requests.py b/marble-plotter-dous/src/main/python/eureka/requests.py new file mode 100644 index 0000000..4c27f29 --- /dev/null +++ b/marble-plotter-dous/src/main/python/eureka/requests.py @@ -0,0 +1,76 @@ +import urllib.request, urllib.error, urllib.parse +from eureka import __version__ as client_version +import gzip +import io + + +class EurekaHTTPException(Exception): + pass + + +class Request(urllib.request.Request): + """ + Instead of requiring a version of `requests`, we use this easy wrapper around urllib2 to avoud possible + version conflicts with people own software. + """ + def __init__(self, url, method="GET", data=None, headers=None, + origin_req_host=None, unverifiable=False): + self.method = method + self._opener = urllib.request.build_opener() + self._opener.addheaders = [ + ('User-agent', 'python-eureka v%s' % client_version) + ] + urllib.request.Request.__init__(self, url, data=data, headers=headers or {}, + origin_req_host=origin_req_host, unverifiable=unverifiable) + + def get_method(self): + return self.method + + @classmethod + def create(cls, method, url, data=None, headers=None): + headers = headers or {} + request = cls(url, method, data=data, headers=headers) + try: + response = request._opener.open(request) + except urllib.error.HTTPError as e: + return Response(e.code, e.read(), url, method) + + content = response.read() + info = response.info() + if "gzip" in info.get("Content-Encoding", "").lower(): + data = io.StringIO(content) + gzipper = gzip.GzipFile(fileobj=data) + content = gzipper.read() + + return Response(response.getcode(), content, url, method) + + +class Response(object): + def __init__(self, status_code, content, url, method): + self.status_code = status_code + self.content = content + self.url = url + self.method = method + + def raise_for_status(self): + if not (200 <= self.status_code < 300): + raise EurekaHTTPException("HTTP %s: %s" % (self.status_code, self.content)) + + def __repr__(self): + return "" % self.status_code + + +def get(url, data=None, headers=None): + return Request.create("GET", url, data, headers) + + +def post(url, data=None, headers=None): + return Request.create("POST", url, data, headers) + + +def put(url, data=None, headers=None): + return Request.create("PUT", url, data, headers) + + +def delete(url, data=None, headers=None): + return Request.create("DELETE", url, data, headers) \ No newline at end of file diff --git a/marble-plotter-dous/src/main/python/index.html b/marble-plotter-dous/src/main/python/index.html new file mode 100644 index 0000000..aee7b10 --- /dev/null +++ b/marble-plotter-dous/src/main/python/index.html @@ -0,0 +1,12 @@ + + + + Display Image + + + + + + \ No newline at end of file diff --git a/marble-plotter-dous/src/main/python/server.py b/marble-plotter-dous/src/main/python/server.py new file mode 100644 index 0000000..cca08ea --- /dev/null +++ b/marble-plotter-dous/src/main/python/server.py @@ -0,0 +1,250 @@ +# Rest Server +from flask import Flask, jsonify, abort, request +# Eureka client +from eureka.client import EurekaClient +# Background tasks +import threading +import atexit +import logging +import socket +import netifaces as ni +import sys +import os +import time + +# Plotter libs +from io import BytesIO +import pymongo +from pymongo import MongoClient +import numpy as np +import matplotlib.pyplot as plt +import pandas as pd +import base64 +import datetime + +DATABASE_NAME = 'marble' +POSTS_COLLECTION = 'posts' +PROCESSED_POSTS_COLLECTION = 'processed_posts' + +pool_time = 5 # Seconds + +# variables that are accessible from anywhere +commonDataStruct = {} +# lock to control access to variable +dataLock = threading.Lock() +# thread handler +yourThread = threading.Thread() + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Global variables +app_name = "plotter-dous" + +try: + ni.ifaddresses('eth0') + app_ip = ni.ifaddresses('eth0')[2][0]['addr'] +except Exception: + app_ip = "localhost" +app_host = socket.getfqdn() +app_port = 8084 +secure_app_port = 8443 + +eureka_url = "http://registry:1111/eureka/" + + +def create_app(): + app = Flask(__name__) + + def interrupt(): + global yourThread + yourThread.cancel() + + def doStuff(): + global commonDataStruct + global yourThread + with dataLock: + # TODO: Handle what happens when eureka goes down + try: + commonDataStruct['ec'].heartbeat() + except Exception: + logger.info("Registering to Eureka...") + try: + commonDataStruct['ec'].register(initial_status="UP") + logger.info("Registered to Eureka.") + commonDataStruct['ec'].heartbeat() + except Exception as e: + logger.warning( + "Caught exception while trying to register in Eureka: " + str(e) + ". Will retry again shortly.") + exc_type, exc_obj, exc_tb = sys.exc_info() + fname = os.path.split( + exc_tb.tb_frame.f_code.co_filename)[1] + print((exc_type, fname, exc_tb.tb_lineno)) + + # Set the next thread to happen + yourThread = threading.Timer(pool_time, doStuff, ()) + yourThread.start() + + def doStuffStart(): + # Do initialisation stuff here + # no spaces or underscores, this needs to be url-friendly + + commonDataStruct['ec'] = EurekaClient(app_name, + ip_address=app_ip, + eureka_url=eureka_url, + eureka_domain_name="", + data_center="MyOwn", + port=app_port, + secure_port=None, + use_dns=False, + region="none", + prefer_same_zone=False, + context="", + host_name=app_host, + vip_address=app_name, + secure_vip_address=app_name) + + global yourThread + # Create your thread + yourThread = threading.Timer(pool_time, doStuff, ()) + yourThread.start() + + # Initiate + doStuffStart() + # When you kill Flask (SIGTERM), clear the trigger for the next thread + atexit.register(interrupt) + return app + + +class ChartResponse(object): + + def __init__(self, name, description="", type="Image", customType=None, jobId=None, options={}, data={}, images={}): + self.id = None + self.name = name + self.description = description + self.type = type + self.customType = customType + self.jobId = jobId + self.options = options + self.data = data + self.images = images + self.createdAt = None + + +def plotTopic(topicName, options): + polarity = None + + chartName = options['title'] + chartDescription = options['description'] + + client = MongoClient('mongodb', 27017) + db = client[DATABASE_NAME] + + posts_collection = db.get_collection(POSTS_COLLECTION) + processed_posts_collection = db.get_collection(PROCESSED_POSTS_COLLECTION) + + invalid_plot = False + if (options['type'] == "scatter"): + logger.debug("Plotting scatter.") + + collection = options.get('collection', PROCESSED_POSTS_COLLECTION) + point_size = options.get('point_size', 2) + color = options.get('color', 'green') + y_axis_field = options.get('y_axis_field', 'polarity') + y_min = options.get('y_min', None) + y_max = options.get('y_max', None) + + if (collection == POSTS_COLLECTION): + posts = posts_collection.find( + {'topicName': topicName}).sort('createdAt', pymongo.ASCENDING) + else: + posts = processed_posts_collection.find( + {'topicName': topicName}).sort('createdAt', pymongo.ASCENDING) + + dates_axis = [] + y_axis = [] + for post in posts: + if (y_axis_field in post): + dates_axis.append(post['createdAt']) + y_axis.append(post[y_axis_field]) + + dates = [pd.to_datetime(d) for d in dates_axis] + + fig = plt.figure(1, figsize=(11, 6)) + + plt.title(chartName) + plt.xlabel('createdAt') + plt.ylabel(y_axis_field) + + # the scatter plot: + axScatter = plt.subplot(111) + axScatter.scatter(x=dates, y=y_axis, s=point_size, color=color) + + # set axes range + plt.xlim(dates[0], dates[len(dates) - 1]) + if y_min == None: + y_min = min(y_axis) + if y_max == None: + y_max = max(y_axis) + plt.ylim(y_min, y_max) + + my_plot = plt.gcf() + imgdata = BytesIO() + + # my_plot.show() + + my_plot.savefig(imgdata, format='png') + + encoded_chart = base64.b64encode(imgdata.getvalue()) + else: + invalid_plot = True + + client.close() + + if invalid_plot: + return None + + singleChart = { + "id": None, + "name": chartName, + "description": chartDescription, + "type": "Figure List", + "customType": "", + "jobId": None, + "options": {}, + "data": {}, + "figures": [encoded_chart.decode('ascii')], + #"figures": [], + "createdAt": None + } + + response = { + "charts": [ + singleChart + ] + } + + return response + + +app = create_app() + + +@app.route('/api/plot', methods=['POST']) +def process(): + print(request) + if not request.json or not 'topicName' or not 'options' in request.json: + abort(400) + response = plotTopic( + request.json['topicName'], request.json.get('options', {})) + if (response != None): + return jsonify(response), 200 + else: + return "", 500 + + +if __name__ == '__main__': + app.run(host="0.0.0.0", port=app_port) + # plotTopic("Apple Microsoft", { + # 'title': 'Titlte', 'description': 'Dscription'}) + #input("Press Enter to continue...") diff --git a/marble-plotter-dous/src/main/python/test.py b/marble-plotter-dous/src/main/python/test.py new file mode 100644 index 0000000..2506802 --- /dev/null +++ b/marble-plotter-dous/src/main/python/test.py @@ -0,0 +1,56 @@ +from io import BytesIO +import gridfs +from pymongo import MongoClient + +client = MongoClient('mongodb', 27017) + +DATABASE_NAME = 'marble' +POSTS_COLLECTION = 'posts' +PROCESSED_POSTS_COLLECTION = 'processed_posts' + +# Temp Variables +topicName = 'Apple Microsoft' + +db = client[DATABASE_NAME] +fs = gridfs.GridFS(db) + +posts_collection = db.get_collection(POSTS_COLLECTION) +processed_posts_collection = db.get_collection(PROCESSED_POSTS_COLLECTION) + +posts = posts_collection.find({'topicName': topicName}) + +# for post in posts: +# print(post['_id']) + +import numpy as np +import matplotlib.pyplot as plt + + +N = 50 +x = np.random.rand(N) +y = np.random.rand(N) +colors = np.random.rand(N) +area = np.pi * (15 * np.random.rand(N))**2 # 0 to 15 point radii + +plt.scatter(x, y, s=area, c=colors, alpha=0.5) +#plt.show() +my_plot = plt.gcf() + +imgdata = BytesIO() +my_plot.savefig(imgdata, format='png') +my_plot.savefig('imgdata.png', format='png') +imgdata.seek(0) +b = fs.put(imgdata, filename="foo.png", topicName=topicName) +print(b) + +chart_1 = Chart( + title='Sample Post', + content='Some engaging content', + author='Scott' +) +chart_1.save() # This will perform an insert + + +class Chart(Document): + title = StringField() + published = BooleanField() diff --git a/marble-plotter-simple/pom.xml b/marble-plotter-simple/pom.xml index 2baf70a..c7e1906 100644 --- a/marble-plotter-simple/pom.xml +++ b/marble-plotter-simple/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.marble marble-plotter-simple - 1.0.1-RELEASE + 1.0.2-RELEASE jar @@ -19,11 +19,7 @@ - - org.marble - marble-model - 1.0.1-RELEASE - + diff --git a/marble-plotter-simple/src/main/docker/README.md b/marble-plotter-simple/src/main/docker/README.md index d436502..4d1164d 100644 --- a/marble-plotter-simple/src/main/docker/README.md +++ b/marble-plotter-simple/src/main/docker/README.md @@ -7,7 +7,7 @@ [Marble](http://marble.miguelfc.com/) is an opinion mining platform, modular and open sourced. This is part of a PhD project, being developed by [Miguel Fernandes](http://miguelfernandes.com/) for the University of Vigo, under the supervision of Ana Fernández Vilas and Rebeca Díaz Redondo. -Marble is composed of several modules. This one, marble-plotter-simple, is a plotter module, providing charts and report to the system. +Marble is composed of several modules. This one, marble-plotter-simple, is a plotter module, providing charts and reports to the system. All the modules of the platform them described in the [Github project](https://github.com/miguelfc/marble). diff --git a/marble-plotter-simple/src/main/java/org/marble/plotter/simple/service/PlotterServiceImpl.java b/marble-plotter-simple/src/main/java/org/marble/plotter/simple/service/PlotterServiceImpl.java index f00425d..ba739d5 100644 --- a/marble-plotter-simple/src/main/java/org/marble/plotter/simple/service/PlotterServiceImpl.java +++ b/marble-plotter-simple/src/main/java/org/marble/plotter/simple/service/PlotterServiceImpl.java @@ -112,11 +112,12 @@ public List plot(PlotterInput input) { log.info("Creating plot for topic <" + topicName + ">..."); // Here starts the execution - Chart plot = new Chart(); - plot.setTopic(topic); - plot.setName(plotTitle); - plot.setDescription(plotDescription); - plot.setType(plotGraphic); + Chart chart = new Chart(); + chart.setTopic(topic); + chart.setName(plotTitle); + chart.setType(Chart.TYPE_GOOGLE_CHART); + chart.setDescription(plotDescription); + chart.setCustomType(plotGraphic); Map singleData = new HashMap<>(); singleData.put("cols", getPostsColumns(topic, plotType)); @@ -171,10 +172,10 @@ public List plot(PlotterInput input) { break; } - plot.setData(singleData); - plot.setOptions(getOptions(topic, plotType)); + chart.setData(singleData); + chart.setOptions(getOptions(topic, plotType)); - plotList.add(plot); + plotList.add(chart); return plotList; } diff --git a/marble-preprocessor-simple/pom.xml b/marble-preprocessor-simple/pom.xml index 0d93fee..e5a7009 100644 --- a/marble-preprocessor-simple/pom.xml +++ b/marble-preprocessor-simple/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.marble marble-preprocessor-simple - 1.0.1-RELEASE + 1.0.2-RELEASE jar @@ -19,11 +19,6 @@ - - org.marble - marble-model - 1.0.1-RELEASE - diff --git a/marble-processor-nltk/pom.xml b/marble-processor-nltk/pom.xml index f23d2e9..45414ce 100644 --- a/marble-processor-nltk/pom.xml +++ b/marble-processor-nltk/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.marble marble-processor-nltk - 1.0.0-RELEASE + 1.0.1-RELEASE 1.8 diff --git a/marble-processor-nltk/src/main/docker/README.md b/marble-processor-nltk/src/main/docker/README.md index 8cbe463..eaec5c8 100644 --- a/marble-processor-nltk/src/main/docker/README.md +++ b/marble-processor-nltk/src/main/docker/README.md @@ -1,6 +1,7 @@ # Supported tags and respective `Dockerfile` links -- [`1.0.0-RELEASE`, `latest` (*Dockerfile*)](https://github.com/miguelfc/marble/blob/76d5017b1438a4c2c00fd401b9f1a8f8bcdb73db/marble-core/src/main/docker/Dockerfile) +- [`1.0.1-RELEASE`, `latest` (*Dockerfile*)] +- [`1.0.0-RELEASE`] # What is Marble? diff --git a/marble-processor-nltk/src/main/python/server.py b/marble-processor-nltk/src/main/python/server.py index 3361a31..336fadf 100644 --- a/marble-processor-nltk/src/main/python/server.py +++ b/marble-processor-nltk/src/main/python/server.py @@ -35,7 +35,7 @@ app_ip = ni.ifaddresses('eth0')[2][0]['addr'] except Exception: app_ip = "localhost" -app_host = socket.gethostname() +app_host = socket.getfqdn() app_port = 8080 secure_app_port = 8443 diff --git a/marble-processor-simple/pom.xml b/marble-processor-simple/pom.xml index d3d2c1c..a3f7fc2 100644 --- a/marble-processor-simple/pom.xml +++ b/marble-processor-simple/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.marble marble-processor-simple - 1.0.1-RELEASE + 1.0.2-RELEASE jar @@ -19,11 +19,6 @@ - - org.marble - marble-model - 1.0.1-RELEASE - diff --git a/marble-processor-sklearn/pom.xml b/marble-processor-sklearn/pom.xml index b66aadf..dc47ef2 100644 --- a/marble-processor-sklearn/pom.xml +++ b/marble-processor-sklearn/pom.xml @@ -4,7 +4,7 @@ 4.0.0 org.marble marble-processor-sklearn - 1.0.0-RELEASE + 1.0.1-RELEASE 1.8 diff --git a/marble-processor-sklearn/src/main/docker/README.md b/marble-processor-sklearn/src/main/docker/README.md index 22fc8ee..64a54c6 100644 --- a/marble-processor-sklearn/src/main/docker/README.md +++ b/marble-processor-sklearn/src/main/docker/README.md @@ -1,6 +1,7 @@ # Supported tags and respective `Dockerfile` links -- [`1.0.0-RELEASE`, `latest`] +- [`1.0.1-RELEASE`, `latest`] +- [`1.0.0-RELEASE`] # What is Marble? diff --git a/marble-processor-sklearn/src/main/python/server.py b/marble-processor-sklearn/src/main/python/server.py index 22bf50a..4db6d07 100644 --- a/marble-processor-sklearn/src/main/python/server.py +++ b/marble-processor-sklearn/src/main/python/server.py @@ -41,7 +41,7 @@ app_ip = ni.ifaddresses('eth0')[2][0]['addr'] except Exception: app_ip = "localhost" -app_host = socket.gethostname() +app_host = socket.getfqdn() app_port = 8080 secure_app_port = 8443 diff --git a/marble-processor-stanford/pom.xml b/marble-processor-stanford/pom.xml index 262282f..12151fc 100644 --- a/marble-processor-stanford/pom.xml +++ b/marble-processor-stanford/pom.xml @@ -3,7 +3,7 @@ 4.0.0 org.marble marble-processor-stanford - 1.0.1-RELEASE + 1.0.2-RELEASE jar @@ -19,11 +19,6 @@ - - org.marble - marble-model - 1.0.1-RELEASE - diff --git a/pom.xml b/pom.xml index e59f496..c3c0e5c 100644 --- a/pom.xml +++ b/pom.xml @@ -18,6 +18,7 @@ marble-plotter-simple marble-processor-nltk marble-processor-sklearn + marble-plotter-dous @@ -41,6 +42,11 @@ org.springframework.boot spring-boot-starter-web + + org.marble + marble-model + 1.0.2-RELEASE +
{{chart.description? chart.description : 'This chart does not have a description defined.'}}
{{chart.description? chart.description : 'This chart does + not have a description defined.'}}