/** * 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.util.ArrayList; import java.util.Date; 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.PUT; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.core.Context; import xbdd.util.StatusHelper; import xbdd.webapp.factory.MongoDBAccessor; import xbdd.webapp.util.Coordinates; import xbdd.webapp.util.Field; import com.mongodb.BasicDBList; import com.mongodb.BasicDBObject; import com.mongodb.DB; import com.mongodb.DBCollection; import com.mongodb.DBCursor; import com.mongodb.DBObject; @Path("/rest/feature") public class Feature { private final MongoDBAccessor client; private static int MAX_ENVIRONMENTS_FOR_A_PRODUCT = 10; @Inject public Feature(final MongoDBAccessor client) { this.client = client; } @SuppressWarnings("unchecked") /** * Uses the '.+' regexp on featureId to allow for symbols such as slashes in the id * * @param String featureId The featureId to get the history for * @return DBObjet Returns the past feature status for the given featureId */ @GET @Path("/rollup/{product}/{major}.{minor}.{servicePack}/{featureId:.+}") public DBObject getFeatureRollup(@BeanParam final Coordinates coordinates, @PathParam("featureId") final String featureId) { final List<BasicDBObject> features = new ArrayList<BasicDBObject>(); final DB db = this.client.getDB("bdd"); final DBCollection collection = db.getCollection("features"); final DBCollection summary = db.getCollection("summary"); final BasicDBObject example = coordinates.getRollupQueryObject(featureId); final DBCursor cursor = collection.find(example, new BasicDBObject("id", 1).append("coordinates.build", 1).append("calculatedStatus", 1) .append("originalAutomatedStatus", 1).append("statusLastEditedBy", 1)); try { while (cursor.hasNext()) { final DBObject doc = cursor.next(); final BasicDBObject rollup = new BasicDBObject() .append("build", ((DBObject) doc.get("coordinates")).get("build")) .append("calculatedStatus", doc.get("calculatedStatus")) .append("originalAutomatedStatus", doc.get("originalAutomatedStatus")) .append("statusLastEditedBy", doc.get("statusLastEditedBy")); features.add(rollup); } } finally { cursor.close(); } final BasicDBObject returns = new BasicDBObject() .append("coordinates", coordinates.getRollupCoordinates().append("featureId", featureId).append("version", coordinates.getVersionString())); final DBObject buildOrder = summary.findOne(coordinates.getQueryObject()); final List<String> buildArray = (List<String>) buildOrder.get("builds"); final List<BasicDBObject> orderedFeatures = new ArrayList<BasicDBObject>(); for (String build : buildArray) { for (BasicDBObject feature : features) { if (feature.get("build").equals(build)) { orderedFeatures.add(feature); break; } } } returns.append("rollup", orderedFeatures); return returns; } /** * Uses the '.+' regexp on featureId to allow for symbols such as slashes in the id * * @param String featureId The featureId to get the history for * @return DBObjet Returns the the current features state and details (environments, tips, steps and scenarios) */ @GET @Path("/{product}/{major}.{minor}.{servicePack}/{build}/{featureId:.+}") public DBObject getFeature(@BeanParam final Coordinates coordinates, @PathParam("featureId") final String featureId) { final DB db = this.client.getDB("bdd"); final DBCollection tips = db.getCollection("features"); final BasicDBObject example = new BasicDBObject().append("id", featureId).append("coordinates", coordinates.getReportCoordinates()); final DBObject feature = tips.findOne(example); if (feature != null) { Feature.embedTestingTips(feature, coordinates, db); } return feature; } @SuppressWarnings("unchecked") protected void updateTestingTips(final DB db, final Coordinates coordinates, final String featureId, final DBObject feature) { final DBCollection tips = db.getCollection("testingTips"); final List<DBObject> elements = (List<DBObject>) feature.get("elements"); for (final DBObject scenario : elements) { if (scenario.get("testing-tips") != null) { final String tipText = (String) scenario.get("testing-tips"); final String scenarioId = (String) scenario.get("id"); final BasicDBObject tipQuery = coordinates.getTestingTipsCoordinatesQueryObject(featureId, scenarioId); DBObject oldTip = null; // get the most recent tip that is LTE to the current coordinates. i.e. sort in reverse chronological order and take the // first item (if one exists). final DBCursor oldTipCursor = tips.find(tipQuery) .sort(new BasicDBObject("coordinates.major", -1).append("coordinates.minor", -1) .append("coordinates.servicePack", -1).append("coordinates.build", -1)).limit(1); try { if (oldTipCursor.hasNext()) { oldTip = oldTipCursor.next(); } } finally { oldTipCursor.close(); } if (oldTip != null) { // if there is an old tip... final String oldTipText = (String) oldTip.get("testing-tips"); // get it and... if (!tipText.equals(oldTipText)) {// compare it to the current tip to it, if they're not the same... final DBObject newTip = new BasicDBObject("testing-tips", tipText).append("coordinates", coordinates.getTestingTipsCoordinates(featureId, scenarioId)) .append("_id", coordinates.getTestingTipsId(featureId, scenarioId)); tips.save(newTip);// then save this as a new tip. } } else { // no prior tip exists, add this one. final DBObject newTip = new BasicDBObject("testing-tips", tipText).append("coordinates", coordinates.getTestingTipsCoordinates(featureId, scenarioId)) .append("_id", coordinates.getTestingTipsId(featureId, scenarioId)); tips.save(newTip);// then save this as a new tip. } } scenario.removeField("testing-tips"); } } /** * Uses the '.+' regexp on featureId to allow for symbols such as slashes in the id * * @param String featureId The featureId to make changes to * @return DBObjet Returns the the features new state if changes were made and returns null if bad JSON was sent */ @PUT @Path("/{product}/{major}.{minor}.{servicePack}/{build}/{featureId:.+}") @Consumes("application/json") public DBObject putFeature(@BeanParam final Coordinates coordinates, @PathParam("featureId") final String featureId, @Context final HttpServletRequest req, final DBObject feature) { feature.put("calculatedStatus", StatusHelper.getFeatureStatus(feature)); try { final DB db = this.client.getDB("bdd"); final DBCollection collection = db.getCollection("features"); final BasicDBObject example = coordinates.getReportCoordinatesQueryObject().append("id", featureId); final DBObject report = collection.findOne(example); // get the differences/new edits // Detect if the edits caused a change feature.put("statusLastEditedBy", req.getRemoteUser()); feature.put("lastEditOn", new Date()); final BasicDBList edits = updateEdits(feature, report); feature.put("edits", edits); updateTestingTips(db, coordinates, featureId, feature); // save testing tips / strip them out of the document. updateEnvironmentDetails(db, coordinates, feature); collection.save(feature); Feature.embedTestingTips(feature, coordinates, db); // rembed testing tips. return feature;// pull back feature - will re-include tips that were extracted prior to saving } catch (final Throwable th) { th.printStackTrace(); return null; } } /** * Goes through each environment detail on this feature and pushes each unique one to a per-product document in the 'environments' * collection. * * @param db * @param coordinates * @param feature */ @SuppressWarnings("unchecked") public void updateEnvironmentDetails(final DB db, final Coordinates coordinates, final DBObject feature) { final DBCollection env = db.getCollection("environments"); final List<DBObject> elements = (List<DBObject>) feature.get("elements"); final BasicDBObject envQuery = coordinates.getQueryObject(Field.PRODUCT); // pull back the "product" document containing all the environments. DBObject productEnvironments = env.findOne(envQuery); // if one doesn't exist then create it. if (productEnvironments == null) { productEnvironments = new BasicDBObject(); productEnvironments.put("coordinates", coordinates.getObject(Field.PRODUCT)); } // pull back the list of environments List<Object> envs = (List<Object>) productEnvironments.get("environments"); // if the list doesn't exist then create it. if (envs == null) { envs = new BasicDBList(); productEnvironments.put("environments", envs); } final List<String> titleCache = new ArrayList<String>(); // go through each scenario, pull out the environment details and add them to the back of the list. for (final DBObject scenario : elements) { String notes = (String) scenario.get("environment-notes"); if (notes != null) { notes = notes.trim(); if (notes.length() > 0) { if (!titleCache.contains(notes)) { titleCache.add(notes); } } } } // go through each unique environment detail, remove it if it is already in the list and append to the end. for (final String environmentDetail : titleCache) { envs.remove(environmentDetail); envs.add(environmentDetail); } // if the list gets too long, truncate it on a LRU basis. if (envs.size() > MAX_ENVIRONMENTS_FOR_A_PRODUCT) { envs = envs.subList(envs.size() - MAX_ENVIRONMENTS_FOR_A_PRODUCT, envs.size()); productEnvironments.put("environments", envs); } // save the list back. env.save(productEnvironments); } @SuppressWarnings("unchecked") public static void embedTestingTips(final DBObject feature, final Coordinates coordinates, final DB db) { final DBCollection tips = db.getCollection("testingTips"); final List<DBObject> elements = (List<DBObject>) feature.get("elements"); for (final DBObject scenario : elements) { DBObject oldTip = null; final BasicDBObject tipQuery = coordinates.getTestingTipsCoordinatesQueryObject((String) feature.get("id"), (String) scenario.get("id")); // get the most recent tip that is LTE to the current coordinates. i.e. sort in reverse chronological order and take the first // item (if one exists). final DBCursor oldTipCursor = tips.find(tipQuery) .sort(new BasicDBObject("coordinates.major", -1).append("coordinates.minor", -1) .append("coordinates.servicePack", -1).append("coordinates.build", -1)).limit(1); try { if (oldTipCursor.hasNext()) { oldTip = oldTipCursor.next(); scenario.put("testing-tips", oldTip.get("testing-tips")); } } finally { oldTipCursor.close(); } } } private BasicDBList updateEdits(final DBObject feature, final DBObject previousVersion) { BasicDBList edits = (BasicDBList) feature.get("edits"); if (edits == null) { edits = new BasicDBList(); } final BasicDBList newEdits = new BasicDBList(); final BasicDBObject edit = new BasicDBObject() .append("name", feature.get("statusLastEditedBy")) .append("date", feature.get("lastEditOn")) .append("prev", previousVersion.get("calculatedStatus")) .append("curr", feature.get("calculatedStatus")) .append("stepChanges", constructEditStepChanges(feature, previousVersion)); newEdits.add(edit); newEdits.addAll(edits); return newEdits; } private BasicDBList constructEditStepChanges(final DBObject currentVersion, final DBObject previousVersion) { final BasicDBList stepChanges = new BasicDBList(); final BasicDBList elements = (BasicDBList) currentVersion.get("elements"); final BasicDBList prevElements = (BasicDBList) previousVersion.get("elements"); if (elements != null) { for (int i = 0; i < elements.size(); i++) { final BasicDBList allSteps = new BasicDBList(); final BasicDBList changes = new BasicDBList(); final BasicDBObject element = (BasicDBObject) elements.get(i); final BasicDBObject prevElement = (BasicDBObject) prevElements.get(i); final String scenarioName = (String) element.get("name"); boolean currManual = false; boolean prevManual = false; // get all scenario steps if ((BasicDBObject) element.get("background") != null) { for (int j = 0; j < ((BasicDBList) ((BasicDBObject) element.get("background")).get("steps")).size(); j++) { final BasicDBObject step = (BasicDBObject) ((BasicDBList) ((BasicDBObject) element.get("background")).get("steps")) .get(j); final BasicDBObject prevStep = (BasicDBObject) ((BasicDBList) ((BasicDBObject) prevElement.get("background")) .get("steps")) .get(j); final String id = (String) step.get("keyword") + (String) step.get("name"); if (((BasicDBObject) step.get("result")).get("manualStatus") != null) { currManual = true; } if (((BasicDBObject) prevStep.get("result")).get("manualStatus") != null) { prevManual = true; } final BasicDBObject compareStep = new BasicDBObject() .append("id", id) .append("curr", step) .append("prev", prevStep); allSteps.add(compareStep); } } if ((BasicDBList) element.get("steps") != null) { for (int j = 0; j < ((BasicDBList) element.get("steps")).size(); j++) { final BasicDBObject step = (BasicDBObject) ((BasicDBList) element.get("steps")).get(j); final BasicDBObject prevStep = (BasicDBObject) ((BasicDBList) prevElement.get("steps")).get(j); final String id = (String) step.get("keyword") + (String) step.get("name"); if (((BasicDBObject) step.get("result")).get("manualStatus") != null) { currManual = true; } if (((BasicDBObject) prevStep.get("result")).get("manualStatus") != null) { prevManual = true; } final BasicDBObject compareStep = new BasicDBObject() .append("id", id) .append("curr", step) .append("prev", prevStep); allSteps.add(compareStep); } } for (int j = 0; j < allSteps.size(); j++) { formatStep(changes, (BasicDBObject) allSteps.get(j), currManual, prevManual); } // only add if changes have been made if (changes.size() > 0) { final BasicDBObject singleScenario = new BasicDBObject() .append("scenario", scenarioName) .append("changes", changes); stepChanges.add(singleScenario); } } } return stepChanges; } private void formatStep(final BasicDBList changes, final BasicDBObject step, final boolean currManual, final boolean prevManual) { String currState, currCause, prevState, prevCause; final BasicDBObject currStep = ((BasicDBObject) step.get("curr")); if (((BasicDBObject) currStep.get("result")).get("manualStatus") != null) { currState = (String) ((BasicDBObject) currStep.get("result")).get("manualStatus"); currCause = "manual"; } else { currCause = "auto"; if (currManual) { currState = "undefined"; } else { currState = (String) ((BasicDBObject) currStep.get("result")).get("status"); } } final BasicDBObject prevStep = ((BasicDBObject) step.get("prev")); if (((BasicDBObject) prevStep.get("result")).get("manualStatus") != null) { prevState = (String) ((BasicDBObject) prevStep.get("result")).get("manualStatus"); prevCause = "manual"; } else { prevCause = "auto"; if (prevManual) { prevState = "undefined"; } else { prevState = (String) ((BasicDBObject) prevStep.get("result")).get("status"); } } // only add if different if (!currState.equals(prevState) || !currCause.equals(prevCause)) { final BasicDBObject stateChange = new BasicDBObject() .append("id", step.get("id")) .append("curr", currState) .append("prev", prevState); changes.add(stateChange); } } }