Archives

Archives / 2005 / January
  • Some old articles from CodeProject

    I moved or copied a couple of old ASP.NET articles I wrote from CodeProject to my blog, mainly to keep track of them. They're available under my Articles section here. Old ones, but pretty good primers for new ASP.NET developers.
  • [ASP.NET] An XML based (we)blog with RSS feed

    This is an old article I wrote for CodeProject some time ago. I'm putting it here in my blog to keep track of it. The code is avaliable for download from CodeProject -> http://www.codeproject.com/soap/weblog.asp

    Sample Image - weblog.gif

    Introduction

    Since writing weblogs, or blogging as it is also called, has become pretty popular the last year, I thought of constructing my own blog tool. Blog is a shorter word for a web log, an online (most often public) journal where the author writes down his or her thougths, sometimes around a specific topic. This article describes how to write a pretty simple weblog application and a windows program for writing entries sitting in the system tray.

    Some of the techniques used in this application are XML and XML Schema, Web Services, DataSets, Cache and the Calendar Web Control. Oh, and the XML Control too for transforming the XML weblog into RSS.

    The Web Application

    The web application consists of three parts actually; the web page showing the log and a calendar, a password protected web service for accepting entries and finally a second web page, which transforms the internal XML file into a RSS 2.0 feed via XSL transformation.

    The Windows Application

    The windows application (from now on called the client) is fairly simple in functionality and consists of a single dialog where the user can type in a message and send it over to the web site via a Web Service call.

    The client sits in the system tray all the time, and when the user wants to write a message in his or her weblog, a click with the mouse brings up the dialog, ready for use.

    Using the code

    Let’s go over some of the more interesting parts of the code, starting with the XML format for the weblog data.

    The Weblog XML and Schema

    <?xml version="1.0" standalone="yes"?> <weblog> <logentry>     <id>0a8d4ec3-eec1-4b07-b26f-98bb5561f43c</id>     <logtitle>A title</logtitle> <logtime>2003-01-10T13:28:14.2031250+01:00</logtime> <logtimeGMT>Fri, 10 Jan 2003 13:28:14 GMT</logtimeGMT> <logtext>This is an entry in the weblog.</logtext> </logentry> </weblog> 

    And the XML Schema for the weblog:

    <?xml version="1.0" encoding="utf-8" ?> <xs:schema id="weblog" xmlns="" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata"> <xs:element name="weblog" msdata:IsDataSet="true" msdata:Locale="sv-SE"> <xs:complexType> <xs:choice maxOccurs="unbounded"> <xs:element name="logentry"> <xs:complexType> <xs:sequence>               <xs:element name="id" type="xs:string" minOccurs="0" />               <xs:element name="logtitle" type="xs:string" minOccurs="0" /> <xs:element name="logtime" type="xs:date" minOccurs="0" /> <xs:element name="logtimeGMT" type="xs:string" minOccurs="0" /> <xs:element name="logtext" type="xs:string" minOccurs="0" /> </xs:sequence> </xs:complexType> </xs:element> </xs:choice> </xs:complexType> </xs:element> </xs:schema> 

    As the XML and the schema shows, the weblog consists of a number of log entries containing data for id, logtitle, logtext, logtime and logtimeGMT. The logtimeGMT is for the RSS feed, since it needs to be in RFC 822 format. I couldn’t find any simple way of transforming logtime into GMT with XSLT so I took the lazy path and stored both of them in the XML file. The id tag is a unique id that is given to each new blog entry.

    The weblog web page

    The weblog is presented on the web page by reading the XML file into a DataSet and binding that to a Repeater. I like the Repeater for simple loops like this, why use the more complex DataGrid or DataList when it’s not needed?

    Remember to turn off the ViewState of the Repeater, it’s not needed and will speed up the loading of the page.

    Every call to the page starts by getting the cached DataSet from the XML file. This is done in the Page_Load event.

    Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles MyBase.Load 'allways start with getting our cached dataset dsWebLog = XmlHelper.GetDS() If Not IsPostBack Then SetDates() BindList() End If End Sub 

    The XmlHelper class has a few static methods for reading and writing the XML DataSet.

    The location of the XML file is stored in the ASP.NET configuration file, web.config.

    Public Shared Function GetDS() As DataSet 'get DS from cache Dim ds As DataSet = CType(HttpContext.Current.Cache("dsWebLog"), DataSet) If ds Is Nothing Then ds = New DataSet("weblog") ds.ReadXmlSchema(ConfigurationSettings.AppSettings("xmlSchema")) Try ds.ReadXml(ConfigurationSettings.AppSettings("xmlFile")) Catch ex As Exception 'missing an xml file is perfectly fine, this might be the 'first time the app is used End Try 'store in cache with dependency to the xml file HttpContext.Current.Cache.Insert("dsWebLog", ds, _ New Caching.CacheDependency(ConfigurationSettings.AppSettings("xmlFile"))) End If Return ds End Function

    The cache has a dependency to the XML file, so the .NET Cache will automatically flush the cached DataSet if a new message is added to the XML file.

    To be able to select a certain date, I also added the ASP.NET Calendar control to the page. When the page is loaded I loop through all the dates in the weblog XML DataSet and select all the dates in the calendar that has an entry in the weblog. When someone clicks a certain date in the calendar, the DataSet is filtered before it’s bound to the Repeater.

    Private Sub Calendar1_SelectionChanged(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles Calendar1.SelectionChanged dateFilter = Calendar1.SelectedDate.AddDays(1).ToString SetDates() BindList() End Sub 

    Before the DataSet is bound to the Repeater, the log entries are sorted and only the top 50 entries are shown. This (as so much else in the sample app) can be set in the web.config file.

     Private Sub BindList() 'get a dataview from a copy of the cached dataset Dim dvWeblog As DataView = dsWebLog.Copy.Tables(0).DefaultView 'filter on date? If dateFilter <> "" Then dvWeblog.RowFilter = "logtime < '" & dateFilter & "'" End If 'sort it by date and time dvWeblog.Sort = "logtime desc" 'copy maximum nr of rows to show Dim dtWeblog As DataTable = XmlHelper.GetTopRows(dvWeblog, ConfigurationSettings.AppSettings("maxrows")) 'bind the sorted and stripped log to the repeater weblogList.DataSource = dtWeblog weblogList.DataBind() End Sub 

    The DataSet is filtered by setting the RowFilter property of the DataView. The .NET Cache has a pointer to our cached DataSet, and the cached DataSet has a pointer to the DataView, so if we don’t take a copy of the DataSet, the RowFilter property will be the same for other users of the cached DataSet. Something I discovered the hard way...

     'get a dataview from a copy of the cached dataset Dim dvWeblog As DataView = dsWebLog.Copy.Tables(0).DefaultView 

    The method called GetTopRows is also located in the XmlHelper class, and it copies a specific number of rows from the log to be displayed in the page.

     Public Shared Function GetTopRows(ByVal dv As DataView, _ ByVal Rows As Integer) As DataTable Dim dtReturn As DataTable Dim cRow As Integer Dim maxRows As Integer maxRows = dv.Count dtReturn = dv.Table.Clone() For cRow = 0 To (Rows - 1) If cRow = maxRows Then Exit For dtReturn.ImportRow(dv(cRow).Row) Next Return dtReturn End Function

    The weblog client

    The client is made up from a single dialog, which starts up minimized to the system tray, i.e. as an icon in the status area of the desktop. The dialog has a TextBox for the title, RichTextBoxfor the body text and a couple of buttons for sending the log entry to the Web Service and for hiding or closing the program.

    So, to post some text to the weblog Web Service, the user types some text in the title textbox and in the body textbox, then presses the Send-button. I thought the Web Service should have some way of protection, so therefore the call is authenticated with a password sent in the SOAP Header. The password is stored in a config file, and I use the built in .NET ConfigurationSettings file (WeblogClient.exe.config) for this.

    Update: To be able to type in formatted text with different colors and fonts, and also to be able to type in HTML or XML tags, the text in the RichTextBox is first converted to HTML (RichTextBoxUtil.ConvertToHTML()). You can have a look at the utility class called RichTextBoxUtil.vb to see how it is done. Note that the utility doesn't handle links yet.

     Private Sub Send_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles SendButton.Click 'Send message to the Weblog via SOAP/Webservice Dim wsWeblog As New Weblog.Weblog() 'get password from the config file (WeblogClient.exe.config) Dim password As String = ConfigurationSettings.AppSettings("password") If password Is Nothing Then ConfigError("password") End If 'this is our SOAP Header class Dim authentication As New Weblog.AuthHeader() authentication.password = password wsWeblog.AuthHeaderValue = authentication 'get the Web Service URL from the config file Dim URL As String = ConfigurationSettings.AppSettings("wsPostURL") If URL Is Nothing Then ConfigError("URL") End If 'set the correct URL for the Web Service wsWeblog.Url = URL         'send HTML converted text to the weblog webservice         wsWeblog.PostMessage(TextBox1.Text, _ RichTextBoxUtil.ConvertToHTML(RichTextBox1)) WindowState = FormWindowState.Minimized HideMe() 'clear out the textbox Me.RichTextBox1.Clear()         Me.TextBox1.Clear() End Sub 

    The URL for the Web Service is also stored in the config file (WebLogClient.exe.config), which must be located in the same directory as the weblog client.

    The Web Service

    The Web Service method for receiving and storing the posted message is quite small. It's one simple method, and it first checks the SOAP Header and compares the password, then it stores the posted message to the weblog.

    <WebMethod(Description:="Post a message to the weblog. An authentication SOAP header is mandatory."), SoapHeader("authentication")> _ Public Function PostMessage(ByVal title As String, ByVal message As String) As Integer If authentication.password = ConfigurationSettings.AppSettings("password")_ Then 'password is ok, stor message in the XML file XmlHelper.AddMessage(title, message) Else Throw New Exception("Invalid password") End If End Function

    The password is (as so much else) stored in the web.config file.

    The AddMessage() method just adds a new DataRow in the weblog DataSet and saves it back to XML. The method also creates a unique id for this posting. The new DataRow is added at the top of the DataSet. The XML file is stored at the location specified by the web.config file (default is at c:\weblog.xml).

     Public Shared Sub AddMessage(ByVal title As String, _ ByVal message As String) Dim dsWebLog As DataSet = XmlHelper.GetDS Dim drNew As DataRow drNew = dsWebLog.Tables(0).NewRow         drNew.Item("id") = Guid.NewGuid.ToString         drNew.Item("logtitle") = title drNew.Item("logtime") = Now drNew.Item("logtimeGMT") = Format(Now, "r") 'RFC 822 format  drNew.Item("logtext") = message dsWebLog.Tables(0).Rows.InsertAt(drNew, 0) 'insert it at beginning 'store xml again dsWebLog.WriteXml(ConfigurationSettings.AppSettings("xmlFile")) End Sub 

    The weblog RSS 2.0 feed

    More and more of the weblogs on the Internet provide an RSS feed of it’s content. I've seen different explanations about what RSS stands for. This was taken from the RSS specification:

    “RSS is a Web content syndication format. Its name is an acronym for Really Simple Syndication. RSS is dialect of XML.”

    But some people say "RDF Site Summary", and RDF stands for Resource Description Framework, which is a foundation for processing metadata. It really doesn't matter, it's a great way to publish content in a simple XML way.

    RSS has been around since 1999 and I’ve tried to create a very simple RSS feed by reading the RSS 2.0 Specification located at http://backend.userland.com/rss

    Just for the “fun” of it, I tried to use XSL Transformation to turn the weblog XML file into the correct RSS format. So, I created a new WebForm ASPX page, and removed everything except the Page header from it, and added a ContentType attribute to it for text/xml.

    <%@ Page contenttype="text/xml" Language="vb" AutoEventWireup="false" Codebehind="rss20.aspx.vb" Inherits="Weblog.rss20"%> 

    Then I drag/dropped an ASP.NET XML Control to the page and added some code in code-behind to point out the XML file and the XSL file.

    Private Sub Page_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Dim doc As XmlDocument = New XmlDocument() doc.Load(ConfigurationSettings.AppSettings("xmlFile")) Dim trans As XslTransform = New XslTransform() trans.Load(ConfigurationSettings.AppSettings("RSSxslFile")) Xml1.Document = doc Xml1.Transform = trans End Sub 

    This is the XSL file used to transform the XML file:

    <xsl:stylesheet version='1.0' xmlns:xsl='http://www.w3.org/1999/XSL/Transform'> <xsl:template match="/"> <rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/"> <channel> <title>My weblog</title> <link>http://localhost/weblog/</link> <description>Just another weblog...</description> <managingEditor>someone@somewhere.com (My Name)</managingEditor> <language>en-us</language> <xsl:for-each select='weblog/logentry'> <item> <link>http://localhost/weblog/Item.aspx?id=<xsl:value-of select='id'/></link>             <guid isPermaLink="false"><xsl:value-of select='id'/></guid>             <title><xsl:value-of select='logtitle'/></title>             <description><xsl:value-of select='logtext'/></description>             <pubDate><xsl:value-of select='logtimeGMT'/></pubDate> </item> </xsl:for-each> </channel> </rss> </xsl:template> </xsl:stylesheet> 

    The XSL file loops through each log-entry and writes them out within description and pubDate tags. Publication date needs to be in RFC 822 format (GMT-format) according to the RSS spec, that’s why I use that field in the XML file.

    Update: The XSL file has been updated now so is also writes out a title, guid and a link to the blog entry.

    One bad thing with this page is that it will write out every record in the weblog, something I took care of in the web page. It shouldn’t be too hard to sort and filter out the top 50 records or so in the way it’s done in the web page, but I leave that for later updates.

    Points of interest

    I could have created the RSS feed in a number of different ways, but I’ve always wanted to try out the ASP.NET XML Control, so that’s why I went for that. I found out that you can do a number of things with XSL Transformation, but wow, it’s pretty complicated.

    As I wrote earlier in the article, it’s easy to forget that the .NET Cache keeps pointers to reference type objects and if you change data in objects you get from the Cache, you are actually changing the object kept in the .NET Cache. Remember that when using the Cache object, you might mess things up for other visitors to the webby. As long as you store value types in the Cache you don’t have to worry.

    Updates

    Update 1 - I added a title to the blog entry, mostly because it looks best i different RSS readers if there's a title for each blog entry. For the sake of RSS, I also added a guid for each entry. I also added code to transform some of the formatted text in the RichTextBox into HTML. It shouldn't be any problems to cut and paste colored and indented source code or HTML/XML into the RichTextBox. It looks pretty good on the HTML page. Note that it doesn't handle hyperlinks yet.

  • [ASP.NET] Extending the Label Control

    This is an old article I wrote for CodeProject once. I'm putting it on my blog just to keep track of it. The code is available for download at CodeProject -> http://www.codeproject.com/aspnet/textcontrolext.asp

    Introduction

    This small article shows how to extend the ASP.NET Label Control, so it reads resource strings from an XML file, which is stored in memory as a Cached DataSet with cache-dependency to the XML-file. Each web page has its own XML-file where texts are stored.

    A control like this might come in handy if you need to translate your web application into other languages, or if the owner of the web site wants to change the “static” texts in the web pages by just editing the XML-files.

    The program could easily be extended so the resource texts are read from another source, like a database, Web Service or a resource file, but I wanted to keep this article fairly small and easy, just to show the general idea.

    I’ve used this technique in production, but since we use a web farm in production, having the resource strings in files isn’t that convenient, so instead I keep all strings in a database. Its also a good idea to give the web application owner, a nice interface for changing the texts. I might write another article to show you how to do this.

    Some of the .NET features used in the sample project:

    • Custom controls
    • DataSet
    • XML
    • Cache

    Code overview

    The Control is called TextControl because it handles texts from a resource of strings. Perhaps a name like Text or StringResource is better, but TextControl will have to do. Feel free to change it :)

    The central part of the code is made up from a single custom control, which inherits from, and extends the ASP.NET Label control. The control also uses a helper class called XMLResource, which will be explained later.

    [DefaultProperty("Key"), ToolboxData("<{0}:TextControl runat=server></{0}:TextControl>")] public class TextControl : System.Web.UI.WebControls.Label 

    The reason I wanted to inherit from the Label control instead of the base Control class is that I want to use the rendering functions that are already implemented in the Label control for different fonts, colors and stuff. I’m lazy ;)

    The TextControl needs to know what text string it should get from its resource of texts, so therefore I added a new property to it called Key. I might as well have re-used the inherited Text property that is already in there, but I had better use for that Text property (shown later).

    private string key; [Bindable(true), Category("Appearance"), DefaultValue(""), Description("ID/Key of the resource text to get")] public string Key { get { return key; } set { key = value; } } 

    There really isn’t much to say about the code snippet above. Its important to have the right attributes or the property might not show up in the property dialog in VisualStudio.NET.

    The final (and interesting part) of the control is the code that renders the output in both run-time and design-time. To do this you must override the Render() method of the Label class. When the ASPX page renders content, the page calls this Render() method for each control on the page.

    protected override void Render(HtmlTextWriter output) { try { //get the text string from the resource //the Text property is used as "default" value Text = new XMLResource().GetValueFromKey(Key, Text); //call base class for rendering base.Render(output); } catch //catch design-time exceptions  { //render the Text property with brackets String tmpText = Text; Text = "[" + Text + "]"; //call base class for rendering base.Render(output); //put back the default text without brackets Text = tmpText; } } 

    As you can see in the code above, I’m using a try/catch block to handle errors, that might happen when we’re getting the text from the resource. I’m getting the texts from an XML-file with the same name as the ASPX file (as described in the introduction), so if the TextControl sits in an ASPX page called Default.aspx, the resource XML file for that page is called Default.xml and located in the same directory as the ASPX page.

    To get the name of the ASPX file, I use the CurrentExecutionFilePath property of the Request object. The Request object is accessed via the current HttpContext, which isn’t available at design-time, so therefore I catch the exception thrown and show the default text within square brackets. You might as well prevent the exception in the XMLResource class (shown below) but this works fine for me.

    If the requested string (based on the Key) isn’t found or if an exception occurs, I show the default text, which I get from the re-used Text property of the parent Label control.

    So, how do we get the text from the resource file? All is done in the XMLResource class, which I’ll try to explain below. The XMLResource class has two methods; one public to get the requested text string and one private, which get the XML file from a cached DataSet. First the public method:

    public String GetValueFromKey(String key, String defaultValue) { DataSet ds = GetResourceDS(); //filter out everything except our resource string in a dataview DataView dv = ds.Tables[0].DefaultView; dv.RowFilter = "key = '" + key + "'"; if(dv.Count > 0) //key found, show the value return dv[0]["value"].ToString(); else //key not found return default value return defaultValue; } 

    It’s pretty simple. Get a DataSet, which contains all the resource strings for this ASPX page. Then filter out our text string based on the key. If the key isn’t found, the default value is returned.

    private DataSet GetResourceDS() { //Try to get the DataSet from the Cache first //Note that this one bombs at design time, (we have no HttpContext) //but this exception is caught in the custom control DataSet ds = (DataSet)HttpContext.Current.Cache[HttpContext.Current. Request.CurrentExecutionFilePath]; if(ds == null) { //no DataSet in the Cache, get a new one and store it in the cache //get the text-id from the resource file for our web-page ds = new DataSet("resourceStrings"); //build the name of the xml file from the name of the web // page we're sitting in String fileName = HttpContext.Current. Request.CurrentExecutionFilePath; fileName = fileName.Substring(0, fileName.LastIndexOf(".")) + ".xml"; try { //read ds from the xml file ds.ReadXml(HttpContext.Current.Server.MapPath(fileName)); } catch (System.IO.FileNotFoundException) { //xml file not found, we will return the empty DataSet } //put in Cache with dependency to the xml file HttpContext.Current.Cache.Insert(HttpContext.Current. Request.CurrentExecutionFilePath, ds, new CacheDependency(HttpContext.Current. Server.MapPath(fileName))); } return ds; } 

    The GetResourceDS method is longer, but not too complicated. First it tries to get the DataSet from the built in .NET Cache object, which is very cool. As a Cache-key I use the name of the ASPX page. If the DataSet wasn’t in the Cache, I create a new DataSet and read in the content from the XML file, which should have the same name as the ASPX page where the TextControl is used (as explained earlier).

    The cached DataSet should expire whenever someone updates the XML file, right? So, therefore I add a cache-dependency, which points at the XML file. So now, when the file is updated, the Cache object detects this and removes the cached DataSet from its memory.

    The ASP.NET Cache might be even more useful if you decide to put all your texts into one big, single XML file. There is no reason to read that XML file every time a TextControl is rendered, right?

    Using the code

    To use the TextControl from VisualStudio.NET, you need to add the assembly containing the control to the toolbox. Load up VisualStudio.NET and create a new ASP.NET application. Then add a WebForm and bring it up in design view. Now right click in the Toolbox containing all the other built-in server controls and select “Customize Toolbox...” from the popup menu. Now select the tab named “.NET Framework Components” and use the “Browse...” button to find the DLL containing the TextControl. The sample project has the TextControl in the MyControls.dll assembly.

    VS.NET will automatically find and activate the TextControl inside the MyControls DLL. You can put the control on any of the different tabs in the Toolbox, but I prefer to have it together with the other server controls.

    The TextControl is now ready for use on a web page. Create a WebForm called WebForm1.aspx and drag/drop the TextControl from the Toolbox on it in design view. Select the TextControl on the ASPX page and look at the property dialog. Change the Key property to “key1” (for example) and optionally put some text in the Text property. It should look similar to this in the HTML view:

    <form id="Form1" method="post" runat="server"> <cc1:TextControl id="TextControl1" runat="server" Key="key1"> </cc1:TextControl> </form> 

    Note that VS.NET automatically added a reference to MyControls in the project view and also added code at the top of the file for registering the custom control:

    <%@ Register TagPrefix="cc1" Namespace="MyControls" Assembly="MyControls" %>

    The only thing to do now is to create the XML file containing the resource strings. Just add a new XML file called WebForm1.xml in the same directory as your newly created ASPX page. The XML file is pretty simple and looks like this:

    <?xml version="1.0" encoding="utf-8" ?> <resourceStrings> <string> <key>key1</key> <value>sampleValue 1</value> </string> <string> <key>key2</key> <value>sampleValue 2</value> </string> </resourceStrings> 

    Each string node in the XML file makes up a key/value pair in the resource file.

    The web page is ready for testing and should show a plain web page with the text “sampleValue 1” on it. If you change the text “sampleValue 1” in the XML file into something else and reload the page, it should show the new value at once.

    The XMLResource class can also be used from code behind to get and set texts at run-time. Just call the public method GetValueFromKey() from the code behind:

    //set the text of the label in run-time Label1.Text = new MyControls.XMLResource().GetValueFromKey("key2", Label1.Text); 

    Conclusion

    As I wrote earlier, there are lots of things you could (and should) do with this code to use it in production, but I hope this little article can get some of you into making your own controls, which is very fun and really, really useful. I recommend doing similar controls for Buttons, Hyperlinks and so on, the owner of the web site might want to change the texts of all the submit buttons to SAVE or DO IT...

    Some things that I’ve added to this control in a production site:

    • XML Schema for validating the XML resource file (never trust a customer to write perfect XML ;)
    • Password protected web form for updating the XML texts
  • Must-have tools for .NET developers

    This is a pretty old one, but still, the tools on this list are just great. I still have to look at CodeSmith and Regulator.

    The tools on the list are these:

    • NUnit to write unit tests
    • NDoc to create code documentation
    • NAnt to build your solutions
    • CodeSmith to generate code
    • FxCop to police your code
    • Snippet Compiler to compile small bits of code
    • Two different switcher tools, the ASP.NET Version Switcher and the Visual Studio .NET Project Converter
    • Regulator to build regular expressions
    • .NET Reflector to examine assemblies

    I would like to add ReSharper (from the JetBrain people) to the list, a quite good refactoring tool.

  • [Java] Setting WSS4J properties for Username Token at run-time

    I managed to dynamically set the username and the other properties that normally goes into the client WSDD file. Again, many thanks to the nice people in the WSS4J mailing list.

    First you have to modify the client WSDD file from the tutorial and comment out the properties. If you leave them in there, I believe those values will be used. So, the client_deploy.wsdd looks something like this:

    <deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
     <transport name="http" pivot="java:org.apache.axis.transport.http.HTTPSender"/>
      <globalConfiguration >
       <requestFlow >
        <handler type="java:org.apache.ws.axis.security.WSDoAllSender" >
        <!-- parameters removed -->
        </handler>
       </requestFlow >
      </globalConfiguration >
    </deployment>

    Then you have to modify the StockServiceClient.java code somewhat to be able to set the handler properties

    package samples.stock.client;

    import java.rmi.RemoteException;
    import javax.xml.rpc.ServiceException;
    import org.apache.axis.EngineConfiguration;
    import org.apache.axis.configuration.FileProvider;
    import org.apache.axis.client.Stub;
    import java.rmi.Remote;
    import org.apache.ws.security.handler.WSHandlerConstants;
    import org.apache.ws.security.WSConstants;
    import org.apache.ws.security.message.token.UsernameToken;

    public class StockServiceClient
    {
        public StockServiceClient()
        {
        }

        public static void main(String[] args) throws ServiceException,
                RemoteException
        {
            if (args.length == 0)
            {
                System.out.println("Usage:\njava StockServiceClient [symbol]");
                return;
            }
            
            //modify the path to the client_deploy.wsdd
            EngineConfiguration config = new FileProvider("client_deploy.wsdd");
            StockQuoteServiceService locator = new StockQuoteServiceServiceLocator(config);
           
            Remote remote = locator.getPort(StockQuoteService.class);
            Stub axisPort = (Stub)remote;
            axisPort._setProperty(WSHandlerConstants.ACTION, WSHandlerConstants.USERNAME_TOKEN);
            axisPort._setProperty(UsernameToken.PASSWORD_TYPE, WSConstants.PASSWORD_DIGEST);
            axisPort._setProperty(WSHandlerConstants.USER, "wss4j");
            axisPort._setProperty(WSHandlerConstants.PW_CALLBACK_CLASS, "samples.stock.client.PWCallback");

            //possible to pass a callback instance by ref instead of a class as above
            //PWCallback pwCallback = new PWCallback();
            //axisPort._setProperty(WSHandlerConstants.PW_CALLBACK_REF, pwCallback);

            //Need to cast 
            StockQuoteService service = (StockWss01SoapBindingStub)axisPort;

            System.out.println("Calling service...");
            float quote = service.getQuote(args[0]);
            System.out.println("stock quote service returned " + args[0] + ": "
                    + quote);
        }
    }

    Note that there are several ways to read the WSDD file dynamically, you can also define "axis.ClientConfigFile" as a system property.

    In this sample you're still using a client WSDD file, specifying the handler (WSDoAllSender) Axis should be calling when the SOAP request is sent. It's possible to code so that you don't need the WSDD file at all, but I'll try that at a later state.

    I'm going to be quite busy travelling the next week, but I'll try to make a simple Java Axis/Wss4j client call a ASP.NET service with a UsernameToken added. It'll be fun.

  • [Java] Simple WSS4J with Axis Tutorial

    This is a simple tutorial for getting started with WSS4J. It’s based on a tutorial posted on the WSS4J mailing list by Rami Jaamour, but I’ve added a few clarifications and variants of the code samples. Rami should get all credits for this tutorial; I’m just a newbie trying to learn how to use this tool!

    Updates

    2006-03-29 - If you get an exception like this one below, it is recommended to look at which Axis version your are using and consider Axis version 1.2:

    Exception in thread "main" java.lang.IllegalAccessError: tried to access method org.apache.axis.SOAPPart.setCurrentMessage(Ljava/lang/Object;I)V from class org.apache.ws.axis.security.WSDoAllSender
    at org.apache.ws.axis.security.WSDoAllSender.invoke(WSDoAllSender.java:365)
    at org.apache.axis.strategies.InvocationStrategy.visit(InvocationStrategy.java:71)

    2005-01-29 - This "article" has been updated a couple of times now. Both with information regarding the xalan.jar problems, and how to set the UserName Token dynamically.

    Credits

    As I wrote above, all cred should go to Rami Jaamour because most of the stuff below is written by him. My thanks to the nice guys in the WSS4J mailing list - Ashok Shah, Werner Dittmann, Yves Langisch and others.

    The Future

    I've added a few things myself to this tutorial, and I'll keep adding things as I learn more. I'll also connect this tutorial with a Username Token service written in ASP.NET as soon as possible. After that we'll see what happens. I'd like to encrypt and sign the stuff too in the future...

    Introduction

    WSS4J can be used for securing web services deployed in virtually any application server, but it includes special support for Axis. WSS4J ships with handlers that can be used in Axis-based web services for an easy integration. These handlers can be added to the service deployment descriptor (wsdd file) to add a WS-Security layer to the web service. This is a step by step tutorial for deploying a simple service with Username Token.

    Prereqs

    To run this tutorial, you must install a JDK (of course). I suggest JDK 1.4.2_04 or 1.5.0. Then you need an application server. I’ve personally used version jakarta-tomcat-4.1.31. Then you need to download and install Axis (version 1.2) and WSS4J. Getting hold of WSS4J and the other jars you may need can be quite tricky. One way is to download Maven and checkout and build WSS4J through it. That’s what I did (not without problems though).

    If you have problems getting the needed jar files let me know and I'll try to add them to this space for download. I've compiled the wss4j.jar package and made it available for download here.

    You don’t really need a Java code editor, but it helps. Personally I use Eclipse and Lomboz (a J2EE plug-in for Eclipse).

    Installing WSS4J

    1. Download the WSS4J binaries or build it from sources
    2. Copy the contents (the jar files) of the WSS4J lib directory to your Axis WEB-INF/lib directory. Many jar files will already exist. Most of them will already exist there but you can just overwrite them all.
    3. You may need to restart Tomcat unless you have automatic deployment/class loading turned on. Check the Axis Happiness Page (typically at http://localhost:8080/axis), make sure that the XML Security (xmlsec.jar) is listed under the "Optional Components" section.

    Creating the service

    1. This tutorial will secure the StockQuoteService which ships with the sample code with Axis. If you deploy the sample web apps that ships with Axis you don’t need to do anything more. Look at the Axis docs on how to install it properly. Unless you have one already, create a deployment descriptor (deploy.wsdd) file with the following contents:


    <deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
     <service name="stock-wss-01" provider="java:RPC" style="document" use="literal">
      <parameter name="className" value="samples.stock.StockQuoteService"/>
      <parameter name="allowedMethods" value="getQuote"/>
      <parameter name="scope" value="application"/>
     </service>
    </deployment>

    It doesn’t matter where you put this file.

    1. deploy the service (using AxisAdmin):

    java org.apache.axis.client.AdminClient -lhttp://localhost:8080/axis/services/AdminService deploy.wsdd

    The AdminClient class depends on a load of jar-files, so to deploy this I created a bat-file that looked like this:

    setlocal

    set CLASSPATH=%CLASSPATH%;C:\axis-1_2RC2\lib\axis.jar;C:\axis-1_2RC2\lib\jaxrpc.jar;C:\axis-1_2RC2\lib\commons-logging.jar;C:\axis-1_2RC2\lib\commons-discovery.jar;C:\axis-1_2RC2\lib\saaj.jar;

    java org.apache.axis.client.AdminClient -lhttp://localhost:8080/axis/services/AdminService test-deploy.wsdd

    endlocal

    You have to change the bat-file to reflect where you’ve put your axis jar files naturally.

    Creating the Client

    1. Use WSDL2Java to generate the client service bindings (a number of soap client classes):

      java org.apache.axis.wsdl.WSDL2Java -o . -Nhttp://fox:8080/axis/services/stock-wss-01 samples.stock.client http://fox:8080/axis/services/stock-wss-01?wsdl

      Again, the wsdl2java needs a number of jar files to work properly, so I created a new bat-file to help out with that. The bat-file looks like this:

      setlocal

      set CLASSPATH=%CLASSPATH%;C:\axis-1_2RC2\lib\axis.jar;C:\axis-1_2RC2\lib\jaxrpc.jar;C:\axis-1_2RC2\lib\commons-logging.jar;C:\axis-1_2RC2\lib\commons-discovery.jar;C:\axis-1_2RC2\lib\saaj.jar;C:\axis-1_2RC2\lib\wsdl4j.jar;

      java org.apache.axis.wsdl.WSDL2Java -o . -Nhttp://localhost:8080/axis/services/stock-wss-01 samples.stock.client http://localhost:8080/axis/services/stock-wss-01?wsdl

      endlocal

      A bunch of java classes will be created under samples/stock/client, including the StockQuoteServiceServiceLocator.
    2. Write a simple java console application that uses the generated service locator. For example:

      package samples.stock.client;

      import java.rmi.RemoteException;
      import javax.xml.rpc.ServiceException;

      public class StockServiceClient {
          public StockServiceClient() {
          }
          public static void main(String[] args) throws ServiceException, RemoteException {
              if (args.length == 0) {
                  System.out.println("Usage:\njava StockServiceClient [symbol]");
                  return;
              }
              StockQuoteServiceService locator = new StockQuoteServiceServiceLocator();
              StockQuoteService service = locator.getStockWss01();
              float quote = service.getQuote(args[0]);
              System.out.println("stock quote service returned " + args[0] + ": " + quote);
          }
      }
    3. run the client:

      java samples.stock.client.StockServiceClient XXX

      If all went well, you should get the result:

      stock quote service returned IBM: 55.25

    When using "XXX" as parameter, the service won't try to go out on the Internet to get the real quotes, but just returns a float with the value of 55.25.

    What you’ve created so far is a very simple web service with a simple client that calls it. WSS4J has not been used yet, so this web service call is unprotected. Now it’s time to add a Username Token to the soap call.

    Configuring the Service for Username Token

    1. Modify the deployment descriptor you created above to look like this:

      <deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
       <service name="stock-wss-01" provider="java:RPC" style="document" use="literal">
        <requestFlow>
         <handler type="java:org.apache.ws.axis.security.WSDoAllReceiver">
          <parameter name="passwordCallbackClass" value="PWCallback"/>
          <parameter name="action" value="UsernameToken"/>
         </handler>
        </requestFlow>

        <parameter name="className" value="samples.stock.StockQuoteService"/>
        <parameter name="allowedMethods" value="getQuote"/>
        <parameter name="scope" value="application"/>
       </service>
      </deployment>

      WSDoAllReceiver is an Axis handler located in wss4j.jar package. This is the standard way to deploy an Axis handler. For more details please refer to the Axis handler for WSS4J documentation.
    2. Create a class named PWCallback.java and compile it and put the resulting PWCallback.class file into your Axis WEB-INF/classes directory. In this example I used the default package for simplicity, but you might need to use the fully qualified class name (be consistent with the deployment descriptor).

      The following code snippet shows a simple password callback class:

      import java.io.IOException;
      import javax.security.auth.callback.Callback;
      import javax.security.auth.callback.CallbackHandler;
      import javax.security.auth.callback.UnsupportedCallbackException;
      import org.apache.ws.security.WSPasswordCallback;

      public class PWCallback implements CallbackHandler {
          public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException {
              for (int i = 0; i < callbacks.length; i++) {
                  if (callbacks[i] instanceof WSPasswordCallback) {
                      WSPasswordCallback pc = (WSPasswordCallback)callbacks[i];
                      // set the password given a username
                      if ("wss4j".equals(pc.getIdentifer())) {
                          pc.setPassword("security");
                      }
                  } else {
                      throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");
                  }
              }
          }
      }


    3. Redeploy the service using the bat file you created earlier. Your service should now be expecting a WSS Username Token in the incoming soap request, and clients should send the username "wss4j" and password "security" to get through.

    Configuring the Client for Username Token

    1. run the client we created again:

      java samples.stock.client.StockServiceClient IBM

      You should now get an error:

      Exception in thread "main" AxisFault
       faultCode: {http://schemas.xmlsoap.org/soap/envelope/}Server.generalException
       faultSubcode:
       faultString: WSDoAllReceiver: Request does not contain required Security header

      This is because your client is not configured to send a Username Token yet, so the service is rejecting the request. To fix this, you need to create a callback class in the client, which adds the Username Token to the outgoing soap request.
    2. Create a deployment descriptor file (client_deploy.wsdd) for the client:

      <deployment xmlns="http://xml.apache.org/axis/wsdd/" xmlns:java="http://xml.apache.org/axis/wsdd/providers/java">
       <transport name="http" pivot="java:org.apache.axis.transport.http.HTTPSender"/>
        <globalConfiguration >
         <requestFlow >
          <handler type="java:org.apache.ws.axis.security.WSDoAllSender" >
           <parameter name="action" value="UsernameToken"/>
           <parameter name="user" value="wss4j"/>
           <parameter name="passwordCallbackClass" value="samples.stock.client.PWCallback"/>
           <parameter name="passwordType" value="PasswordDigest"/>
          </handler>
         </requestFlow >
        </globalConfiguration >
      </deployment>
    3. Create the samples.stock.client.PWCallback class:

      package samples.stock.client;

      import java.io.IOException;
      import javax.security.auth.callback.Callback;
      import javax.security.auth.callback.CallbackHandler;
      import javax.security.auth.callback.UnsupportedCallbackException;
      import org.apache.ws.security.WSPasswordCallback;

      /**
       * PWCallback for the Client
       */
      public class PWCallback implements CallbackHandler {

          /**
           * @see javax.security.auth.callback.CallbackHandler#handle(javax.security.auth.callback.Callback[])
           */
          public void handle(Callback[] callbacks) throws IOException,
                          UnsupportedCallbackException {
              for (int i = 0; i < callbacks.length; i++) {
                  if (callbacks[i] instanceof WSPasswordCallback) {
                      WSPasswordCallback pc = (WSPasswordCallback)callbacks[i];
                      // set the password given a username
                      if ("wss4j".equals(pc.getIdentifer())) {
                          pc.setPassword("security");
                      }
                  } else {
                      throw new UnsupportedCallbackException(callbacks[i], "Unrecognized Callback");
                  }
              }
          }
      }
    4. Define the system property axis.ClientConfigFile for your client:

      java -Daxis.ClientConfigFile=client_deploy.wsdd -classpath $AXISCLASSPATH samples.stock.client.StockServiceClient

      Make sure that your CLASSPATH includes the jar files under WEB-INF/lib.

      Another way to do this is to specify the wsdd file in your StockServiceClient to the service locator programmatically:

      ...
      import org.apache.axis.EngineConfiguration;
      import org.apache.axis.configuration.FileProvider;
      ...

      EngineConfiguration config = new FileProvider("client_deploy.wsdd");
      StockQuoteServiceService locator = new StockQuoteServiceServiceLocator(config);
      ...
    5. Run the client, you should get no errors:

      stock quote service returned XXX: 55.25

      Your client is now sending a Username Token in the wsse request header with the username "wss4j" (see client_deploy.wsdd) and password "security" (see the PWCallback implementation).

      Another way to do this is to have the client application set the username and CallbackHandler implementation programmatically instead of using the client_deploy.wsdd file:

      ...
      import org.apache.axis.client.Stub;
      ...

      Remote remote = locator.getPort(StockQuoteService.class);
      Stub axisPort = (Stub)remote;
      axisPort._setProperty(UsernameToken.PASSWORD_TYPE, WSConstants.PASSWORD_DIGEST);
      axisPort._setProperty(WSHandlerConstants.USER, "wss4j");
      axisPort._setProperty(WSHandlerConstants.PW_CALLBACK_REF, pwCallback);

      where "pwCallback" is a reference to a PWCallback implementation. See the Axis handler for WSS4J documentation for more details on this.

      UPDATE: I've tried to set the callback using the techinque above, but without much success. I'll continue trying, and when I get it working I'll update this section again :)


      UPDATE 2: After some testing and teaking and good ideas from people, I got the thing above working. It's all explained in another blog post.
    6. Try modifying your client's PWCallback to return the wrong password, or send the wrong username. The service should reject your requests.

    Known problems

    When I first ran this tutorial myself, I got a stacktrace error that didn’t interrupt the program, but printed out a warning about the xalan.jar package. It looks something like this:

    - Unable to patch xalan function table.
           java.lang.NoSuchFieldException: m_functions
                  at java.lang.Class.getField(Unknown Source)
                  at org.apache.xml.security.Init.registerHereFunction(Init.java:429)
                  at org.apache.xml.security.Init.init(Init.java:124)… (and so on)

    This may have to do with how the xalan.jar package is deployed on your system and what version of xalan you use and the version of JDK. I got the tip from Ashok Shah to make sure I use Java version 1.4.2_04 instead of 1.4.2_06 that I used. I’ve not tried this yet, but I will do.

    UPDATE: I tried to put the xalan.jar in the JAVA_HOME/lib/endorsed/ directory, but it didn't work much better. So I updated to JDK 5.0 and made sure that the xalan.jar package from the WSS4J distribution was available to tomcat and to my client, and behold - it works :)

    UPDATE 2: I got a tip from Martin Stemplinger that the xalan.jar should probably go into the JAVA_HOME/jre/lib/endorsed/ directory. I've not tested it myself, but it sounds right.

     

  • [Java][Interop] Testing WSS4J

    Long time since I wrote anything here, but I've actively started to look at what AXIS and WSS4J has to offer when it comes to do WS-Security in Java and how it interops with web services in .NET using WSE 2.0. It's taken me a while to get the Java environment up and running at home because I had to reinstall the whole box. What I've done now is I've installed Virtual PC 2004 and created an XP image with a complete Java environment

    I've downloaded and installed (so far):

    j2sdk 1.4.2_06
    tomcat
    ant
    axis
    maven (to checkout and built wss4j)
    wss4j
    cvs
    junit
    eclipse (java IDE)
    lomboz (j2ee plugin for eclipse)

    It takes a while to configure maven and all the stuff you need for it (cvs, junit and so on) and get it running and in the end I had to go into the ant script and remove the junit testing :( Biggest problem when working with Axis and Wss4j is the lack of documentation. What I'm following now when I do my experiments are some basic documentation about how to do usernametokens with wss4j written by a kind fellow in the wss4j maillinglist.

    I've just got the basic stuff working with a Java client using Axis calling a web service in tomcat using Axis too. That's the easy part, I'll now try to plug in wss4j and do usernametoken... I'll blog the results here before I go to bed :)