/** * Copyright (C) 2015 Orion Health (Orchestral Development Ltd) * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package xbdd.webapp.resource.feature; import java.io.IOException; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.BeanParam; import javax.ws.rs.Consumes; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.PUT; import javax.ws.rs.Path; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; import org.apache.commons.codec.binary.Base64; import org.apache.log4j.Logger; import org.glassfish.jersey.media.multipart.FormDataContentDisposition; import org.glassfish.jersey.media.multipart.FormDataParam; import xbdd.util.StatusHelper; import xbdd.webapp.factory.MongoDBAccessor; import xbdd.webapp.util.Coordinates; import xbdd.webapp.util.Field; import com.mongodb.AggregationOutput; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.DB; import com.mongodb.DBCollection; import com.mongodb.DBCursor; import com.mongodb.DBObject; import com.mongodb.gridfs.GridFS; import com.mongodb.gridfs.GridFSInputFile; import com.mongodb.util.JSON; @Path("/rest/reports") public class Report { private final MongoDBAccessor client; @Inject public Report(final MongoDBAccessor client) { this.client = client; } Logger log = Logger.getLogger(Report.class); @GET @Path("/{product}/{major}.{minor}.{servicePack}/{build}") @Produces("application/json") public DBObject getReportByProductVersionId(@BeanParam final Coordinates coordinates, @QueryParam("searchText") final String searchText, @QueryParam("viewPassed") final Integer viewPassed, @QueryParam("viewFailed") final Integer viewFailed, @QueryParam("viewUndefined") final Integer viewUndefined, @QueryParam("viewSkipped") final Integer viewSkipped, @QueryParam("start") final String start, @QueryParam("limit") final Integer limit) { final BasicDBObject example = QueryBuilder.getInstance().buildFilterQuery(coordinates, searchText, viewPassed, viewFailed, viewUndefined, viewSkipped, start); final DB db = this.client.getDB("bdd"); final DBCollection collection = db.getCollection("features"); final DBCursor cursor = collection.find(example).sort(Coordinates.getFeatureSortingObject()); try { if (limit != null) { cursor.limit(limit); } final BasicDBList featuresToReturn = new BasicDBList(); while (cursor.hasNext()) { featuresToReturn.add(cursor.next()); } embedTestingTips(featuresToReturn, coordinates, db); return featuresToReturn; } finally { cursor.close(); } } @SuppressWarnings("unchecked") @GET @Produces("application/json") public DBObject getSummaryOfAllReports(@Context final HttpServletRequest req) { final DB db = this.client.getDB("bdd"); final DBCollection collection = db.getCollection("summary"); final DBCollection usersCollection = db.getCollection("users"); final BasicDBObject user = new BasicDBObject(); user.put("user_id", req.getRemoteUser()); final DBCursor userCursor = usersCollection.find(user); DBObject userFavourites; if (userCursor.count() != 1) { userFavourites = new BasicDBObject(); } else { final DBObject uDoc = userCursor.next(); if (uDoc.containsField("favourites")) { userFavourites = (DBObject) uDoc.get("favourites"); } else { userFavourites = new BasicDBObject(); } } final DBCursor cursor = collection.find(); try { final BasicDBList returns = new BasicDBList(); DBObject doc; while (cursor.hasNext()) { doc = cursor.next(); final String product = ((String) ((DBObject) doc.get("coordinates")).get("product")); if (userFavourites.containsField(product)) { doc.put("favourite", userFavourites.get(product)); } else { doc.put("favourite", false); } returns.add(doc); } return returns; } finally { cursor.close(); } } @GET @Produces("application/json") @Path("/featureIndex/{product}/{major}.{minor}.{servicePack}/{build}") public DBObject getFeatureIndexForReport(@BeanParam final Coordinates coordinates, @QueryParam("searchText") final String searchText, @QueryParam("viewPassed") final Integer viewPassed, @QueryParam("viewFailed") final Integer viewFailed, @QueryParam("viewUndefined") final Integer viewUndefined, @QueryParam("viewSkipped") final Integer viewSkipped, @QueryParam("start") final String start) { final BasicDBObject example = QueryBuilder.getInstance().buildFilterQuery(coordinates, searchText, viewPassed, viewFailed, viewUndefined, viewSkipped, start); final DB db = this.client.getDB("bdd"); final DBCollection featuresCollection = db.getCollection("features"); final DBCursor features = featuresCollection.find(example, new BasicDBObject("id", 1).append("name", 1).append("calculatedStatus", 1) .append("originalAutomatedStatus", 1).append("tags", 1).append("uri", 1)) .sort(Coordinates.getFeatureSortingObject()); final BasicDBList featureIndex = new BasicDBList(); try { for (final Object o : features) { featureIndex.add(o); } } finally { features.close(); } return featureIndex; } protected void embedTestingTips(final BasicDBList featureList, final Coordinates coordinates, final DB db) { for (final Object feature : featureList) { Feature.embedTestingTips((DBObject) feature, coordinates, db); } } protected String s4() { return Double.toHexString(Math.floor((1 + Math.random()) * 0x10000)).substring(1); } /** * Generates a GUID */ protected String guid() { return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4(); } /** * go through all the embedded content, store it to GridFS, replace the doc embeddings with a hyperlink to the saved content. */ protected void embedSteps(final DBObject feature, final GridFS gridFS, final Coordinates coordinates) { final BasicDBList elements = (BasicDBList) feature.get("elements"); final String featureId = (String) feature.get("_id"); if (elements != null) { for (int j = 0; j < elements.size(); j++) { final DBObject scenario = (DBObject) elements.get(j); final String scenarioId = (String) scenario.get("_id"); final BasicDBList steps = (BasicDBList) scenario.get("steps"); if (steps != null) { for (int k = 0; k < steps.size(); k++) { final DBObject step = (DBObject) steps.get(k); final BasicDBList embeddings = (BasicDBList) step.get("embeddings"); if (embeddings != null) { for (int l = 0; l < embeddings.size(); l++) { final DBObject embedding = (DBObject) embeddings.get(l); final GridFSInputFile image = gridFS .createFile(Base64.decodeBase64(((String) embedding.get("data")).getBytes())); image.setFilename(guid()); final BasicDBObject metadata = new BasicDBObject().append("product", coordinates.getProduct()) .append("major", coordinates.getMajor()).append("minor", coordinates.getMinor()) .append("servicePack", coordinates.getServicePack()).append("build", coordinates.getBuild()) .append("feature", featureId) .append("scenario", scenarioId); image.setMetaData(metadata); image.setContentType((String) embedding.get("mime_type")); image.save(); embeddings.put(l, image.getFilename()); } } } } } } } /** * go through find all the backgrounds elements and nest them in their scenarios (simplifies application logic downstream) */ protected void packBackgroundsInToScenarios(final DBObject feature) { final List<DBObject> packedScenarios = new ArrayList<DBObject>(); // go through all the backgrounds /scenarios final BasicDBList elements = (BasicDBList) feature.get("elements"); if (elements != null) { for (int i = 0; i < elements.size(); i++) { final DBObject element = (DBObject) elements.get(i); if (element.get("type").equals("background")) { // if its a background ((DBObject) elements.get(i + 1)).put("background", element); // push it in to the next element. } else { // assume this is a scenario/other top level element and push it to the packed array. packedScenarios.add(element); } } elements.clear(); elements.addAll(packedScenarios); } } protected void updateSummaryDocument(final DB bdd, final Coordinates coordinates) { // product and version are redundant for search, but ensure they're populated if the upsert results in an insert. final DBObject summaryQuery = new BasicDBObject("_id", coordinates.getProduct() + "/" + coordinates.getVersionString()) .append("coordinates", coordinates.getObject(Field.PRODUCT, Field.VERSION)); final DBCollection summary = bdd.getCollection("summary"); final DBObject summaryObject = summary.findOne(summaryQuery); if (summaryObject != null) { // lookup the summary document @SuppressWarnings("unchecked") final List<String> buildArray = (List<String>) summaryObject.get("builds"); if (!buildArray.contains(coordinates.getBuild())) { // only update it if this build hasn't been added to it before. // Update index document version. summary.update(summaryQuery, new BasicDBObject("$push", new BasicDBObject("builds", coordinates.getBuild())), true, false ); } } else {// if the report doesn't already exist... then add it. summary.update(summaryQuery, new BasicDBObject("$push", new BasicDBObject("builds", coordinates.getBuild())), true, false ); } } @GET @Produces("application/json") @Path("/tags/{product}/{major}.{minor}.{servicePack}/{build}") public DBObject getTagList(@BeanParam final Coordinates coordinates) { final DB bdd = this.client.getDB("bdd"); final DBCollection features = bdd.getCollection("features"); // Build objects for aggregation pipeline // id option: returns each tag with a list of associated feature ids final DBObject match = new BasicDBObject("$match", coordinates.getReportCoordinatesQueryObject()); final DBObject fields = new BasicDBObject("tags.name", 1); fields.put("_id", 0); // comment out for id option final DBObject project = new BasicDBObject("$project", fields); final DBObject unwind = new BasicDBObject("$unwind", "$tags"); final DBObject groupFields = new BasicDBObject("_id", "$tags.name"); // groupFields.put("features", new BasicDBObject("$addToSet", "$_id")); //comment in for id option groupFields.put("amount", new BasicDBObject("$sum", 1)); final DBObject group = new BasicDBObject("$group", groupFields); final DBObject sort = new BasicDBObject("$sort", new BasicDBObject("amount", -1)); final AggregationOutput output = features.aggregate(match, project, unwind, group, sort); // get _ids from each entry of output.result final BasicDBList returns = new BasicDBList(); for (final DBObject obj : output.results()) { final String id = obj.get("_id").toString(); returns.add(id); } return returns; } protected void updateStatsDocument(final DB bdd, final Coordinates coordinates, final BasicDBList features) { // product and version are redundant for search, but ensure they're populated if the upsert results in an insert. final DBCollection statsCollection = bdd.getCollection("reportStats"); final String id = coordinates.getProduct() + "/" + coordinates.getVersionString() + "/" + coordinates.getBuild(); statsCollection.remove(new BasicDBObject("_id", id)); final BasicDBObject stats = new BasicDBObject("coordinates", coordinates.getReportCoordinates()); stats.put("_id", id); final BasicDBObject summary = new BasicDBObject(); stats.put("summary", summary); final BasicDBObject feature = new BasicDBObject(); stats.put("feature", feature); for (final Object ob : features) { final BasicDBList scenarios = (BasicDBList) ((DBObject) ob).get("elements"); if (scenarios != null) { for (final Object o : scenarios) { final String status = StatusHelper.getFinalScenarioStatus((DBObject) o, false).getTextName(); final Integer statusCounter = (Integer) summary.get(status); if (statusCounter == null) { summary.put(status, 1); } else { summary.put(status, statusCounter + 1); } } } } statsCollection.save(stats); } @PUT @Path("/{product}/{major}.{minor}.{servicePack}/{build}") public DBObject putReport(@BeanParam final Coordinates coordinates, final DBObject root) throws IOException { final BasicDBList doc = (BasicDBList) root; final DB grid = this.client.getDB("grid"); final GridFS gridFS = new GridFS(grid); final DB bdd = this.client.getDB("bdd"); final DBCollection features = bdd.getCollection("features"); updateSummaryDocument(bdd, coordinates); for (int i = 0; i < doc.size(); i++) { // take each feature and give it a unique id. final DBObject feature = (DBObject) doc.get(i); final String _id = coordinates.getFeature_Id((String) feature.get("id")); feature.put("_id", _id); embedSteps(feature, gridFS, coordinates); // extract embedded content and hyperlink to it. packBackgroundsInToScenarios(feature); // nest background elements within their scenarios final BasicDBObject featureCo = coordinates.getReportCoordinates(); feature.put("coordinates", featureCo); final BasicDBList newElements = mergeExistingScenarios(features, feature, _id); feature.put("elements", newElements); final String originalStatus = StatusHelper.getFeatureStatus(feature); feature.put("calculatedStatus", originalStatus); feature.put("originalAutomatedStatus", originalStatus); this.log.info("Saving: " + feature.get("name") + " - " + feature.get("calculatedStatus")); this.log.trace("Adding feature:" + JSON.serialize(feature)); features.save(feature); } final DBCursor cursor = features.find(coordinates.getReportCoordinatesQueryObject()); // get new co-ordinates to exclude the "version" // field final List<DBObject> returns = new ArrayList<DBObject>(); try { while (cursor.hasNext()) { returns.add(cursor.next()); } } finally { cursor.close(); } final BasicDBList list = new BasicDBList(); list.addAll(returns); updateStatsDocument(bdd, coordinates, list); return list; } private BasicDBList mergeExistingScenarios(final DBCollection features, final DBObject feature, final String _id) { BasicDBList newElements = (BasicDBList) feature.get("elements"); if (newElements == null) { newElements = new BasicDBList(); } final List<String> newElementIds = new ArrayList<String>(); for (int k = 0; k < newElements.size(); k++) { final DBObject elem = (DBObject) newElements.get(k); final String elem_type = (String) elem.get("type"); if (elem_type.equalsIgnoreCase("scenario")) { newElementIds.add((String) elem.get("id")); } } final DBObject existingFeature = features.findOne(_id); if (existingFeature != null) { final BasicDBList existingElements = (BasicDBList) existingFeature.get("elements"); if (existingElements != null) { for (int j = 0; j < existingElements.size(); j++) { final DBObject element = (DBObject) existingElements.get(j); final String element_type = (String) element.get("type"); if (element_type.equalsIgnoreCase("scenario")) { final String element_id = (String) element.get("id"); if (!newElementIds.contains(element_id)) { newElements.add(element); } } } } } return newElements; } @POST @Path("/{product}/{major}.{minor}.{servicePack}/{build}") @Consumes(MediaType.MULTIPART_FORM_DATA) public Response uploadFile( @BeanParam final Coordinates coord, @FormDataParam("file") final DBObject root, @FormDataParam("file") final FormDataContentDisposition fileDetail) throws IOException { putReport(coord, root); return Response.status(200).entity("success").build(); } }