All Downloads are FREE. Search and download functionalities are using the official Maven repository.

smartrics.rest.fitnesse.fixture.ext.SlimRestFixtureWithSeq Maven / Gradle / Ivy

Go to download

Extensions of the RestFixture. An extension is a RestFixture with some specific/bespoke behaviour not generic enough to make it to the RestFixture itself.

The newest version!
/*  Copyright 2012 Fabrizio Cannizzo
 *
 *  This file is part of RestFixture.
 *
 *  RestFixture (http://code.google.com/p/rest-fixture/) is free software:
 *  you can redistribute it and/or modify it under the terms of the
 *  GNU Lesser General Public License as published by the Free Software Foundation,
 *  either version 3 of the License, or (at your option) any later version.
 *
 *  RestFixture is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public License
 *  along with RestFixture.  If not, see .
 *
 *  If you want to contact the author please leave a comment here
 *  http://smartrics.blogspot.com/2008/08/get-fitnesse-with-some-rest.html
 */
package smartrics.rest.fitnesse.fixture.ext;

import com.patternity.graphic.behavioral.Agent;
import com.patternity.graphic.behavioral.Message;
import com.patternity.graphic.behavioral.Note;
import com.patternity.graphic.dag.Node;
import com.patternity.graphic.layout.sequence.SequenceLayout;
import com.patternity.util.TemplatedWriter;
import fitnesse.util.Base64;
import org.apache.batik.transcoder.TranscoderException;
import org.apache.batik.transcoder.TranscoderInput;
import org.apache.batik.transcoder.TranscoderOutput;
import org.apache.batik.transcoder.image.ImageTranscoder;
import org.apache.batik.transcoder.image.JPEGTranscoder;
import org.apache.batik.transcoder.image.PNGTranscoder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import smartrics.rest.client.RestData.Header;
import smartrics.rest.fitnesse.fixture.PartsFactory;
import smartrics.rest.fitnesse.fixture.RestFixture;
import smartrics.rest.fitnesse.fixture.ext.SlimRestFixtureWithSeq.Model;
import smartrics.rest.fitnesse.fixture.support.CellWrapper;
import smartrics.rest.fitnesse.fixture.support.Tools;

import java.awt.*;
import java.io.*;
import java.util.EventListener;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * An extension of RestFixture that generates a sequence diagrams for a table
 * fixture. Sequence diagrams are generated as SVG files using Patternity Graphic. 
* Each picture can then be transcoded into either PNG or JPG format (via Batik * transcoder API). The format is inferred by the file extension.
* The fixture supports a configuration property. * * * * * *
restfixture.graphs.dirdestination directory where the images with sequence diagrams will be * created. The directory will be created if not existent; the fixture will fail * if the directory can't be created
*
* If the directory specified by restfixture.graphs.dir is created under * FitNesseRoot/files the generated images can be embedded * in the FitNesse pages.
* Including images can be achieved via !img (for PNG or JPG) or * via a specific FitNesse symbol, !svg, for native svg files * {@see smartrics.rest.fitnesse.fixture.SvgImage}
* NOTE: This class only works with Fit runner (not Slim)
* Using the fixture is straightforward. Like the RestFixture, the hostname * needs to be specified. Additionally a new cell needs to be supplied with some * data pertaining the creation of the image file.
* * * * * * *
RestFixtureWithSeqhostnameimage data
*
* Image data is a string containing path to the image file, relative to the * value of the restfixture.graphs.dir directory.
* The string is followed by a list of attributes passed to the SVG generator * for inclusion in the SVG file. for example * * * * * * * *
RestFixtureWithSeqhostnamepost_images/a_post_image.svg viewBox="0 0 200 200" width="100" * height="150"
*
* If the file path contains spaces, it must be included in double quotes. Each * attribute value must be included in double quotes. * * @author smartrics * */ public class SlimRestFixtureWithSeq extends RestFixture { public static final String DEFAULT_GRAPH_DIR_PROPERTY_NAME = "restfixture.graphs.dir"; /** * Model interface for storing http events. * * @author smartrics * */ public interface Model { void delete(String res, String args, String ret); void comment(String body); void get(String res, String args, String ret); void post(String res, String args, String result); void put(String res, String args, String ret); } static final Log LOG = LogFactory.getLog(SlimRestFixtureWithSeq.class); /** * Default directory where the diagrams are generated. * * The value is new File("restfixture"), a directory relative * to the default fitnesse root directory. */ private File graphFileDir; /** * This fixture instance picture name. */ private String pictureName; /** * This fixture instance picture data. * * Picture data is a composite string containig the path to the image file * and a sequence of attributes in the form of name=value. */ private String pictureData; private Model model; private boolean initialised; /** * Listens to events raised by the fixture and captures them in the model. */ private MyFixtureListener myFixtureListener; /** * Svg attributes. */ private Map attributes; /** * File format. */ private String format; @SuppressWarnings("rawtypes") private CellWrapper cell; public SlimRestFixtureWithSeq() { super(); this.initialised = false; LOG.info("Default ctor"); } public SlimRestFixtureWithSeq(String hostName, String pictureData) { super(hostName); this.pictureData = pictureData; this.initialised = false; } public SlimRestFixtureWithSeq(String hostName, String configName, String pictureData) { super(hostName, configName); this.pictureData = pictureData; this.initialised = false; } public SlimRestFixtureWithSeq(PartsFactory partsFactory, String hostName, String configName, String pictureData) { super(partsFactory, hostName, configName); this.pictureData = pictureData; this.initialised = false; } /** * embeds as a <img> with content encoded the model caprured so far. */ public void embed() { cell = row.getCell(1); byte[] content = PictureGenerator.generate(model.toString(), parseAttributes(cell.body()), "template.svg", format); cell.body(getFormatter().gray("")); } public void setModel(Model model) { this.model = model; } @Override protected void initialize(Runner runner) { super.initialize(runner); initializeFields(); createSequenceModel(); initialised = true; String defaultPicsDir = System.getProperty(DEFAULT_GRAPH_DIR_PROPERTY_NAME, "FitNesseRoot/files/restfixture"); String picsDir = getConfig().get(DEFAULT_GRAPH_DIR_PROPERTY_NAME, defaultPicsDir); graphFileDir = new File(picsDir); if (!graphFileDir.exists()) { if (!graphFileDir.mkdirs()) { throw new RuntimeException("Unable to create the diagrams destination dir '" + graphFileDir.getAbsolutePath() + "'"); } else { LOG.info("Created diagrams destination directory '" + graphFileDir.getAbsolutePath() + "'"); } } LOG.info("Generated diagrams directory: '"+ graphFileDir.getAbsolutePath() + "'"); myFixtureListener = new MyFixtureListener(new File(graphFileDir, this.getPictureName()).getAbsolutePath(), model, attributes); setFixtureListener(myFixtureListener); } /** * State of the RestFixtureWithSeq is valid (or true) if both the baseUrl * and picture name are not null. These parameters are the first and second * in input to the fixture. * * @return true if valid. */ @Override protected boolean validateState() { return getBaseUrl() != null && pictureData != null; } @Override protected void notifyInvalidState(boolean state) { if (!state) { throw new RuntimeException( "Both baseUrl and picture data (containing the picture name) need to be passed to the fixture"); } } protected void createSequenceModel() { if (!initialised) { LOG.info("Initialising sequence model"); this.model = new SequenceModel(); } } /** * Overridden as SLIM can't find it. * * Note: for SLIM to find this method it has to be defined in the java file * after the override of the ActionFixture method */ @Override public List> doTable(List> rows) { List> result = super.doTable(rows); myFixtureListener.tableFinished(); return result; } @Override public void setBody() { super.setBody(); } /** * a DELETE generates a message and a return arrows. */ @Override public void DELETE() { super.DELETE(); String res = getLastRequest().getResource(); String args = getLastRequest().getQuery(); String ret = "status=" + getLastResponse().getStatusCode().toString(); model.delete(res, args, ret); } @Override public void comment() { super.comment(); @SuppressWarnings("rawtypes") CellWrapper messageCell = row.getCell(1); String body = messageCell.body(); String plainBody = Tools.fromHtml(body).trim(); model.comment(plainBody); } /** * a GET generates a message and a return arrows. */ @Override public void GET() { super.GET(); String res = getResource(); String args = getLastRequest().getQuery(); String ret = "status=" + getLastResponse().getStatusCode().toString(); model.get(res, args, ret); } /** * a POST generates a message to the resource type, which in turn generates * a create to the resource just created. The resource uri must be defined * in the Location header in the POST response. A return arrow * is then generated. */ @Override public void POST() { super.POST(); String res = getResource(); String id = getIdFromLocationHeader(); // could ever be that the POST to /abc returns a location of /qwe/1 ?? String result = String.format("id=%s, status=%s", id, getLastResponse() .getStatusCode().toString()); String args = getLastRequest().getQuery(); model.post(res, args, result); } /** * a PUT generates a message and a return arrows. */ @Override public void PUT() { super.PUT(); String res = getResource(); String args = getLastRequest().getQuery(); String ret = "status=" + getLastResponse().getStatusCode().toString(); model.put(res, args, ret); } /** * the picture name is the second parameter of the fixture. * * @return the picture name */ String getPictureName() { return pictureName; } void setFixtureListener(MyFixtureListener l) { this.myFixtureListener = l; } private static String getPictureFormat(String pictureName) { int pos = pictureName.indexOf("."); if (pos >= 0) { return pictureName.substring(pos + 1).toLowerCase(); } throw new IllegalArgumentException( "The picture name must terminate with an extension of .svg, .png, .jpg"); } private void initializeFields() { if (!initialised) { String data = pictureData; LOG.info("Picture data = " + pictureData); int[] pos = getPositionOfNextOfTokenOptionallyInDoubleQuotes(data); this.pictureName = data.substring(pos[0], pos[1]); LOG.info("Found picture name: " + pictureName); if (pos[1] < data.length()) { data = data.substring(pos[1] + 1); this.attributes = parseAttributes(data); } this.format = getPictureFormat(pictureName); } } private static Map parseAttributes(String data) { Map foundAttributes = new HashMap(); while (true) { int eqPos = data.indexOf("="); if (eqPos < 0) { break; } String aName = data.substring(0, eqPos); LOG.info("Found attribute name: " + aName); data = data.substring(eqPos + 1); int[] pos = getPositionOfNextOfTokenOptionallyInDoubleQuotes(data); String aVal = data.substring(pos[0], pos[1]); LOG.info("Found attribute val: " + aVal + ", pos[" + pos[0] + ", " + pos[1] + "]"); foundAttributes.put(aName, aVal); if (data.length() - aVal.length() == 0) { break; } data = data.substring(pos[1] + 1); } return foundAttributes; } private static int[] getPositionOfNextOfTokenOptionallyInDoubleQuotes( String data) { String del = " "; int start = 0; if (data.trim().startsWith("\"")) { del = "\""; start = 1; } int end = data.indexOf(del, start + 1); if (end == -1) { end = data.length(); } return new int[] { start, end }; } private String[] guessParts(String res) { String[] empty = new String[] { "?", "" }; if (res == null) { return empty; } String myRes = res.trim(); if (myRes.isEmpty()) { return empty; } int pos = myRes.lastIndexOf("/"); if (pos == myRes.length() - 1) { pos = -1; myRes = myRes.substring(0, myRes.length() - 1); } String[] parts = new String[2]; if (pos >= 0) { parts[0] = myRes.substring(0, pos); parts[1] = myRes.substring(pos + 1); } else { parts[0] = myRes; parts[1] = ""; } return parts; } private String getIdFromLocationHeader() { List
list = getLastResponse().getHeader("Location"); String location = ""; if (list != null && !list.isEmpty()) { location = list.get(0).getValue(); } String[] parts = guessParts(location); return parts[1]; } private String getResource() { String res = getLastRequest().getResource(); if (res.endsWith("/")) { res = res.substring(0, res.length() - 1); } return res; } } /** * Holds the sequence diagram model, specifically abstratcs out the underlying * library constructing the SVG picture. * * @author smartrics * */ class SequenceModel implements Model { private Map resourceToAgentMap; private Node root; private SequenceLayout layout; /** * Hints to the SVG files generator. */ private static final int DEFAULT_FONT_SIZE = 16; private static final int DEFAULT_AGENT_STEP = 150; private static final int DEFAULT_TIME_STEP = 25; SequenceModel() { Message message = new Message(null, null); Node root = new Node(message); SequenceLayout layout = new SequenceLayout(DEFAULT_FONT_SIZE); layout.setAgentStep(DEFAULT_AGENT_STEP); layout.setTimeStep(DEFAULT_TIME_STEP); this.root = root; this.layout = layout; this.resourceToAgentMap = new HashMap(); } public void comment(String text) { root.add(new Node(new Note(Tools.fromHtml(text)))); } public void get(String resource, String query, String result) { message(Message.SYNC, resource, "GET", query, result); } public void post(String resource, String query, String result) { message(Message.SYNC, resource, "POST", query, result); } public void put(String resource, String query, String result) { message(Message.SYNC, resource, "PUT", query, result); } public void delete(String resource, String query, String result) { message(Message.DESTROY, resource, "DELETE", query, result); } public String toString() { return layout.layout(root); } private void message(int type, String resourceTo, String method, String args, String result) { Agent agentTo = agentFor(resourceTo); String methodSignature = method; if (args != null) { methodSignature = method + "(" + args + ")"; } String resultString = ""; if (result != null) { resultString = result; } Message message = new Message(type, agentTo, methodSignature, resultString); root.add(new Node(message)); } private Resource agentFor(String resource) { Resource a = resourceToAgentMap.get(resource); if (a == null) { final boolean isActivable = true; a = new Resource(resource, isActivable); resourceToAgentMap.put(resource, a); } return a; } } /** * A representation of a resource for the purposes of generating the sequence * diagram. * * @author smartrics * */ class Resource extends Agent { public Resource(String type, boolean isActivable) { super(type, "", isActivable); } public String toString() { return isEllipsis() ? "..." : (new StringBuilder()).append(getType()) .toString(); } } /** * A fit.FixtureListener that listens for a table being completed. * the action performed on table completion is the actual graph generation. * * @author smartrics */ class MyFixtureListener implements EventListener { private final Model model; private final String picFileName; private final Map attributes; public MyFixtureListener(String outFileName, Model m, Map attr) { model = m; attributes = attr != null ? attr : new HashMap(); picFileName = outFileName; } /** * generates the sequence diagram with the events collected in the model. */ public void tableFinished() { int pos = picFileName.lastIndexOf("."); String format = "svg"; if (pos > 0) { format = picFileName.substring(pos + 1).toLowerCase(); } byte[] content = PictureGenerator.generate(model.toString(), attributes, "template.svg", format); File f = new File(picFileName); try { f.createNewFile(); } catch (IOException e1) { throw new IllegalArgumentException( "Unable to create output picture file: " + f.getAbsolutePath(), e1); } FileOutputStream fos = null; try { fos = new FileOutputStream(f); } catch (FileNotFoundException e1) { throw new IllegalArgumentException( "Unable to find output picture file: " + f.getAbsolutePath(), e1); } try { fos.write(content); } catch (IOException e1) { throw new IllegalArgumentException( "Unable to write output picture file: " + f.getAbsolutePath(), e1); } try { fos.flush(); } catch (IOException e1) { throw new IllegalArgumentException( "Unable to flush output picture file: " + f.getAbsolutePath()); } try { if (fos != null) { fos.close(); } } catch (IOException e) { } } } /** * Utility class to generate picture wrapping the Patternity Graphic svg * utility. * * @author smartrics * */ class PictureGenerator { private PictureGenerator() { } /** * generates a byte array with the content of the picture from an svg * template. * * @param content * the picture content. * @param attributes * the diagram attributes. * @param svgTemplate * the svg template to use. * @param format * the format of the output rasterised image. * @return a byte array of the picture in the given format. */ public static byte[] generate(String content, Map attributes, String svgTemplate, String format) { ByteArrayOutputStream baos = new ByteArrayOutputStream(); final TemplatedWriter writer = new TemplatedWriter(baos, svgTemplate); StringBuffer sb = new StringBuffer(); for (Map.Entry e : attributes.entrySet()) { sb.append(e.getKey()).append("=\"").append(e.getValue()) .append("\" "); } writer.write(content, sb.toString()); byte[] ret = baos.toByteArray(); try { baos.close(); } catch (IOException e) { SlimRestFixtureWithSeq.LOG .debug("Exception closing byte array output stream"); } if (format.equals("svg")) { return ret; } else { Integer w = null; String widthString = attributes.get("width"); if (widthString != null) { try { w = Integer.parseInt(widthString); } catch (NumberFormatException e) { SlimRestFixtureWithSeq.LOG .debug("Unable to parse width as integer: " + widthString); } } Integer h = null; String heightString = attributes.get("height"); if (heightString != null) { try { h = Integer.parseInt(heightString); } catch (NumberFormatException e) { SlimRestFixtureWithSeq.LOG .debug("Unable to parse height as integer: " + heightString); } } return transcode(ret, format, w, h); } } public static byte[] transcode(byte[] svg, String format, Integer w, Integer h) { ImageTranscoder trans = null; if (format.equals("jpg")) { trans = new JPEGTranscoder(); } else if (format.equals("png")) { trans = new PNGTranscoder(); } else { throw new IllegalArgumentException( "Unsupported raster format. Only jpg and png: " + format); } TranscoderInput input = new TranscoderInput(new ByteArrayInputStream( svg)); ByteArrayOutputStream ostream = new ByteArrayOutputStream(); TranscoderOutput output = new TranscoderOutput(ostream); if (w != null && h != null) { Rectangle aoi = new Rectangle(w, h); trans.addTranscodingHint(JPEGTranscoder.KEY_WIDTH, new Float( aoi.width)); trans.addTranscodingHint(JPEGTranscoder.KEY_HEIGHT, new Float( aoi.height)); trans.addTranscodingHint( ImageTranscoder.KEY_FORCE_TRANSPARENT_WHITE, Boolean.FALSE); trans.addTranscodingHint(JPEGTranscoder.KEY_AOI, aoi); } try { trans.transcode(input, output); } catch (TranscoderException e) { throw new IllegalStateException("Unable to transcode to format: " + format, e); } try { ostream.flush(); } catch (IOException e) { SlimRestFixtureWithSeq.LOG.debug("Unable to flush output stream"); // should be safe to ignore } try { ostream.close(); } catch (IOException e) { SlimRestFixtureWithSeq.LOG.debug("Unable to close output stream"); // should be safe to ignore } return ostream.toByteArray(); } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy