Trace HTTP response’s content in your XUL app

Sometimes you need HTTP requests tracer in your application. There are many pieces of code scattered over the internet, but how to join all of them together?

Idea of this method is stream listener substitution during http-examine-response event. When all content read we just send our own event http-on-examine-response-content. To utilize this method your object should implement nsIObserver interface. Also you should subscribe to http-on-examine-response-content event using @mozilla.org/observer-service;1 service. And of course, you might be interested in http-examine-response and http-examine-request events (just subscribe to them via observer service).

Usage example

let MyCoolObject = {
    downloadedSize: 0,
    observe: function (aChannel, aTopic, aData) {
        switch (aTopic) {
        case "http-on-examine-response-content":
            // counting all responses size, for example
            this.downloadedSize += aData.length;
            break;
        }
    },
};

let ObserverService = Components.classes["@mozilla.org/observer-service;1"]
                      .getService(Components.interfaces.nsIObserverService);
ObserverService.addObserver(MyCoolObject, "http-on-examine-response-content", false);

Component source code

Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");

const CC = Components.classes,
      CI = Components.interfaces,
      ObserverService = CC["@mozilla.org/observer-service;1"].getService(CI.nsIObserverService);

function HttpRequestObserver () {
}
HttpRequestObserver.prototype = {
    classID: Components.ID('{51b8262e-e556-4b41-b530-9145c61a38fa}'),

    observe: function (subject, topic, data) {
        switch (topic) {
        case "profile-after-change":
            this._register();
            break;
        case "http-on-examine-response":
            let interceptor = new StreamInterceptor(subject);
            interceptor.originalListener = subject.QueryInterface(CI.nsITraceableChannel).setNewListener(interceptor);
            break;
        }
    },

    QueryInterface: XPCOMUtils.generateQI([CI.nsIObserver, CI.nsISupports]),

    _register: function () {
        ObserverService.addObserver(this, "http-on-examine-response", false);
    },
};

function StreamInterceptor (aSubject) {
    this.subject = aSubject;
    this.interceptedData = [];
}
StreamInterceptor.prototype = {
    originalListener: null,
    subject: null,

    onDataAvailable: function (request, context, inputStream, offset, count) {
        let binaryInputStream  = CC["@mozilla.org/binaryinputstream;1"].createInstance(CI.nsIBinaryInputStream),
            storageStream      = CC["@mozilla.org/storagestream;1"].createInstance(CI.nsIStorageStream),
            binaryOutputStream = CC["@mozilla.org/binaryoutputstream;1"].createInstance(CI.nsIBinaryOutputStream),
            data;

        binaryInputStream.setInputStream(inputStream);
        storageStream.init(8192, count, null);
        binaryOutputStream.setOutputStream(storageStream.getOutputStream(0));

        data = binaryInputStream.readBytes(count);
        this.interceptedData.push(data);

        binaryOutputStream.writeBytes(data, count);

        try { // XXX see moz-bug 492534
            this.originalListener.onDataAvailable(request, context, storageStream.newInputStream(0), offset, count);
        } catch (e) {
            request.cancel(e.result);
        }
    },

    onStartRequest: function (request, context) {
        try { // XXX see moz-bug 492534
            this.originalListener.onStartRequest(request, context);
        } catch (e) {
            request.cancel(e.result);
        }
    },

    onStopRequest: function (request, context, statusCode) {
        ObserverService.notifyObservers(this.subject, "http-on-examine-response-content", this.interceptedData.join());
        this.originalListener.onStopRequest(request, context, statusCode);
    },

    QueryInterface: XPCOMUtils.generateQI([CI.nsIStreamListener, CI.nsISupports]),
};

const NSGetFactory = XPCOMUtils.generateNSGetFactory([HttpRequestObserver]);

Don’t forget to register your component in chrome.manifest (check XPCOM changes in Gecko 2.0

Leave a comment
Spring DSL

Good news, everyone! I’m back. With totally redesigned blog.

Today’s topic is Spring Domain Specific Language creation.

Sometimes we want to make our spring configs looks simplier. For example:

<bean id="chainedService" class="com.example.blocks.ChainXmlService" parent="baseXmlService">
    <property name="styleSheet" value="xsl/sample.xsl"/>
    <property name="serviceChain">
        <list>
            <bean class="com.example.blocks.HttpProxyBlock">
                <property name="url" value="http://example.com/some/url"/>
            </bean>
            <ref bean="authBlock"/>
        </list>
    </property>
</bean>

This config might look like this:

<web:chain id="chainedService" xsl="xsl/sample.xsl">
    <web:block class="com.example.blocks.HttpProxyBlock" url="http://example.com/some/url"/>
    <web:block ref="authBlock"/>
</web:chain>

Looks fine. How we can do it?

First of all, lets define our namespace in spring configuration. For example: xmlns:web="http://example.com/web-ns"

Now we should create XSD-schema for our namespace and save somewhere on classpath:

<?xml version="1.0" encoding="UTF-8"?>
<xsd:schema xmlns="http://example.com/web-ns"
            xmlns:xsd="http://www.w3.org/2001/XMLSchema"
            targetNamespace="http://example.com/web-ns"
            elementFormDefault="qualified"
            attributeFormDefault="unqualified">

    <!-- defining element chain -->
    <xsd:element name="chain">
        <xsd:complexType>
            <xsd:choice minOccurs="0" maxOccurs="unbounded">
                <xsd:element ref="block"/>
            </xsd:choice>
            <xsd:attribute name="id" type="xsd:ID" use="required"/>
            <xsd:attribute name="xsl" type="xsd:string" use="optional"/>
        </xsd:complexType>
    </xsd:element>

    <!-- definig element block -->
    <xsd:element name="block">
        <xsd:complexType>
            <xsd:attribute name="class" type="xsd:string" use="optional"/>
            <xsd:attribute name="ref" type="xsd:ID" use="optional"/>
            <xsd:any namespace="##any" minOccurs="0"/>
        </xsd:complexType>
    </xsd:element>

</xsd:schema>

Then register out schema in spring. Create META-INF/spring.schemas
and put there some content:

http\://example.com/web-ns.xsd=path/to/our/schema.xsd

Now we are going to create Namespace handler and register it:

package com.example.spring;
import org.springframework.beans.factory.xml.NamespaceHandlerSupport;

public class WebNamespaceHandler extends NamespaceHandlerSupport {
    public void init() {
        registerBeanDefinitionParser("chain", new ChainBeanDefinitionParser());
    }
}

To register this handler – create META-INF/spring.handlers and put
there some content:

http\://example.com/web-ns=com.example.spring.WebNamespaceHandler

Now we should implement definition parser.

package com.example.spring;

import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.ManagedList;
import org.springframework.beans.factory.xml.AbstractBeanDefinitionParser;
import org.springframework.beans.factory.xml.ParserContext;
import org.springframework.util.xml.DomUtils;

import java.util.List;

import org.w3c.dom.Attr;
import org.w3c.dom.Element;
import org.w3c.dom.NamedNodeMap;

import com.example.blocks.ChainXmlService;

public class ChainBeanDefinitionParser extends AbstractBeanDefinitionParser {

	@Override
	protected AbstractBeanDefinition parseInternal(Element element, ParserContext parserContext) {
		BeanDefinitionBuilder root = BeanDefinitionBuilder.genericBeanDefinition();

		// setting generic bean properties
		root.setLazyInit(true);
		root.getRawBeanDefinition().setParentName("baseXmlService");
		root.getRawBeanDefinition().setBeanClass(ChainXmlService.class);
		root.getRawBeanDefinition().setSource(parserContext.extractSource(element));
		if (parserContext.isNested()) {
			root.setScope(parserContext.getContainingBeanDefinition().getScope());
		}

		return parseElement(root, element, parserContext);
	}

	private AbstractBeanDefinition parseElement(BeanDefinitionBuilder root, Element element, ParserContext parserContext) {
		if (element.hasAttribute("xsl")) {
			root.addPropertyValue("styleSheet", element.getAttribute("xsl"));
		}

		List<Element> childs = (List<Element>) DomUtils.getChildElementsByTagName(element, "block");
		if (childs != null && childs.size() > 0) {
			ManagedList blocks = new ManagedList(childs.size());
			for (Element child : childs) {
				if (child.hasAttribute("ref")) {
					blocks.add(parserContext.getRegistry().getBeanDefinition(child.getAttribute("ref")));
				} else {
					blocks.add(parseBlock(child));
				}
			}
			root.addPropertyValue("serviceChain", blocks);
		}

		return root.getBeanDefinition();
	}

	private AbstractBeanDefinition parseBlock(Element element) {
		BeanDefinitionBuilder block = BeanDefinitionBuilder.genericBeanDefinition(element.getAttribute("class"));
		NamedNodeMap attributes = element.getAttributes();
		for (int i = 0; i < attributes.getLength(); i++) {
			Attr attr = (Attr) attributes.item(i);
			String name = attr.getName();
			if ("id".equals(name) || "class".equals(name)) {
				continue;
			}
			block.addPropertyValue(name, attr.getValue());
		}

		return block.getBeanDefinition();
	}

}

That’s all! Now you can use simple domain specific language in your
spring configuration. In my project I’ve implemented my own DSL -
config size reduced and readability increased. It’s big step to human
readable configs :)

P.S. Don’t forget to register schema in your IDE – you will get
absolute support for custom elements (IntelliJ IDEA totally supports
them in spring facets).

1 Comment
Third “first” post
Leave a comment