Thursday, February 25, 2010

Misc Pivot stuff

Padding choices
In wtkx, adding padding usually looks like: styles="{padding:10}". Nice if you want 10px all around, but you can also specify each side styles="{padding:{top:1,left:2,bottom:3,right:4}}".

Adding validators inline
Normally to add a validator to a component, you code something similar to
@WTKX
private TextInput maxFileSize;
...
...
maxFileSize.setValidator(new IntRangeValidator(0, Integer.MAX_VALUE));
Seems like a bit of a waste? In WTKX, you can include any object that has a no-argument constructor. So we can inline the whole thing.
  1. Include the package to your object as part of the xml namespace (notice xmlns:text)
  2. Define the validator.
  3. attach it to your component
<Sheet wtkx:id="exportDialog" title="Generate Crawl List"
    xmlns:wtkx="http://pivot.apache.org/wtkx"
    xmlns:text="org.apache.pivot.wtk.text.validation"
    xmlns="org.apache.pivot.wtk"  styles="{padding:10}">
 <wtkx:define>
     <text:intrangevalidator id="numvalidator"
       minimum="0" maximum="2147483647"/>
 </wtkx:define>
 <content>
...
...
   <textinput textkey="maxFileSize" validator="$numvalidator"
    id="maxFileSize" text="0"/>
...
 </content>
</Sheet>

Tuesday, February 23, 2010

SASL Plain Server

For some reason SUN includes support for sasl PLAIN in their SaslClient, but not SaslServer. Here's a quick hack to implement a plain sasl server. As per rfc4616.txt, there is only message, three UTF-8 strings, max length 255 delimited by null (\u0000) sent from the client. There is no server response.
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import javax.security.auth.callback.Callback;
import javax.security.auth.callback.CallbackHandler;
import javax.security.auth.callback.UnsupportedCallbackException;
import javax.security.sasl.AuthorizeCallback;
import javax.security.sasl.SaslException;
import javax.security.sasl.SaslServer;

/**
 * Simple sasl server to decode a
 * @author toaster
 */
public class PlainSaslServer implements SaslServer
{

    public boolean complete = false;
    private String authID = null;
    private CallbackHandler cbh;
    
    PlainSaslServer(CallbackHandler cbh)
    {
        this.cbh = cbh;
    }

    public String getMechanismName()
    {
        return SaslConstants.SASL_SERVICE;
    }

    public byte[] evaluateResponse(byte[] response) throws SaslException
    {
        String authnID = null;
        String authzID = null;
        String password = null;

        // Decode response as per rfc4616.txt
//       The formal grammar for the client message using Augmented BNF [ABNF]
//       follows.
//
//       message   = [authzid] UTF8NUL authcid UTF8NUL passwd
//       authcid   = 1*SAFE ; MUST accept up to 255 octets
//       authzid   = 1*SAFE ; MUST accept up to 255 octets
//       passwd    = 1*SAFE ; MUST accept up to 255 octets
//       UTF8NUL   = %x00 ; UTF-8 encoded NUL character
//
//       SAFE      = UTF1 / UTF2 / UTF3 / UTF4
//                   ;; any UTF-8 encoded Unicode character except NUL
//
//       UTF1      = %x01-7F ;; except NUL
//       UTF2      = %xC2-DF UTF0
//       UTF3      = %xE0 %xA0-BF UTF0 / %xE1-EC 2(UTF0) /
//                   %xED %x80-9F UTF0 / %xEE-EF 2(UTF0)
//       UTF4      = %xF0 %x90-BF 2(UTF0) / %xF1-F3 3(UTF0) /
//                   %xF4 %x80-8F 2(UTF0)
//       UTF0      = %x80-BF

        int start = 0;
        int end = 0;
        int elementIdx = 0;

        try
        {


            for (byte b : response)
            {
                if (b == '\u0000')
                {
                    // empty string, only authzid allows this
                    if (end - start == 0)
                    {
                        if (elementIdx != 0)
                        {
                           throw new SaslException("null auth data");
                        }

                    } // data, wa-hoo
                    else
                    {
                        String element = new String(response, start, end - start, "UTF-8");
                        start = end + 1;

                        switch (elementIdx)
                        {
                            case 0:
                                authzID = element;
                                break;
                            case 1:
                                authnID = element;
                                break;
                            default:
                                throw new SaslException("Unexpected data in packet");
                        }
                    }
                    elementIdx++;

                }
                end++;
            }

            if (start == end)
            {
                throw new SaslException("null auth data");
            }

            password = new String(response, start, end - start, "UTF-8");


        } catch (UnsupportedEncodingException e)
        {
            throw new SaslException("utf-8 encoding");
        }


        ExternalValidationCallback evc = new ExternalValidationCallback(authnID, password);
        AuthorizeCallback ac = new AuthorizeCallback(authnID, authzID);

        Callback[] cbList = new Callback[2];

        cbList[0] = evc;
        cbList[1] = ac;

        try
        {
            if (password == null || authnID == null)
            {
                throw new SaslException("null auth data");
            }

            cbh.handle(cbList);
            if (!evc.isValidated())
            {
                throw new SaslException("cannot validate password");
            }

            if (!ac.isAuthorized())
            {
                throw new SaslException("user not authorized");
            }

            complete = true;
            return null;

        } catch (UnsupportedCallbackException ex)
        {
            throw new SaslException("unsupported callback", ex);

        } catch (IOException e)
        {
            if (e instanceof SaslException)
            {
                throw (SaslException) e;
            }
            throw new SaslException("Callback error", e);
        }

    }

    public boolean isComplete()
    {
        return complete;
    }

    public String getAuthorizationID()
    {
        if (!complete)
        {
            throw new IllegalStateException("not complete");
        }
        return authID;
    }

    public byte[] unwrap(byte[] incoming, int offset, int len)
    {
        if (!complete)
        {
            throw new IllegalStateException("not complete");
        }
        final byte[] result = new byte[len];
        System.arraycopy(incoming, offset, result, 0, len);
        return result;

    }

    public byte[] wrap(byte[] outgoing, int offset, int len)
    {
        if (!complete)
        {
            throw new IllegalStateException("not complete");
        }

        final byte[] result = new byte[len];
        System.arraycopy(outgoing, offset, result, 0, len);
        return result;

    }

    public Object getNegotiatedProperty(String propName)
    {
        return null;
    }

    public void dispose() throws SaslException
    {
    }
}
This also uses the following callback to validate the client's username and password. The SaslServer will throw a SaslException if the isValidated returns false.
public class ExternalValidationCallback implements Callback
{

    private String username;
    private String password;
    private boolean validated = false;

    public ExternalValidationCallback(String username, String password)
    {
        this.username = username;
        this.password = password;
    }

    public String getPassword()
    {
        return password;
    }

    public String getUsername()
    {
        return username;
    }

    public boolean isValidated()
    {
        return validated;
    }

    public void setValidated(boolean validated)
    {
        this.validated = validated;
    }
}
Now after we have the new callback and server we need to register it as a provider w/in java. This requires that we create a server factory and simple security provider to register the factory. The server factory will need to check the sasl property POLICY_NOPLAINTEXT to see if its allowed to return the PLAIN server.
public class PlainSaslServerFactory implements SaslServerFactory
{

    private static String[] mechanisms =
    {
        "PLAIN"
    };

    public SaslServer createSaslServer(String mechanism, String protocol,
            String serverName,
            Map props, CallbackHandler cbh) throws SaslException
    {

        if (!mechanisms[0].equals(mechanism) || cbh == null)
            return null;
       return new PlainSaslServer(cbh);

    }

    public String[] getMechanismNames(Map props)
    {
        if ("true".equals(props.get(Sasl.POLICY_NOPLAINTEXT)))
            return new String[0];
        return mechanisms;
    }
}
Now, create the java security provider.
public final class MySecurityProvider extends Provider
{

    public MySecurityProvider()
    {
        super("My Provider", 1.0 , "Simple sasl plain provider");
        put("SaslServerFactory.PLAIN", "org.swap.provider.PlainSaslServerFactory");
    }

}
Putting it all together
// register provider
Security.addProvider(new MySecurityProvider());
// create callback
CallbackHandler myHandler = new CallbackHandler() {
public void handle(Callback[] callbacks) throws IOException, UnsupportedCallbackException
    {
 for (Callback cb : callbacks)
        {
            if (cb instanceof ExternalValidationCallback)
            {
                ExternalValidationCallback evc = (ExternalValidationCallback) cb;
                // Add your password validation, unless bob works for you
                evc.setValidated("bob".equals(evc.getUsername() && "password".equals()evc.getPassword());
            }
            else if (cb instanceof AuthorizeCallback)
            {
                AuthorizeCallback ac = (AuthorizeCallback) cb;
                // Add your test to see if client is authorized to use requested
                ac.setAuthorized(true);
            }
            else
            {
                throw new UnsupportedCallbackException(cb, "Unrecognized Callback");
            }
    }
};

SaslServer ss = Sasl.createSaslServer("PLAIN", "myProtocol", serverName, null, myHandler);
ss.evaluateResponse(clientPacket);

Wednesday, February 3, 2010

Pivot Tree List labels and lazy loading

Here's how to lazy load a tree structure in pivot and supply nice(er) labels for all nodes. 1. Create a LabeledList that extends List
public class LabeledList extends ArrayList
{

private V label;

public LabeledList(V label)
{
    this.label = label;
}

public V getLabel()
{
    return label;
}

@Override
public boolean equals(Object o)
{
    return (o instanceof LabeledList && ((LabeledList) o).label.equals(label)
           && super.equals(o));
}

@Override
public int hashCode()
{
    int hash = 5;
    hash = 29 * hash + (this.label != null ? this.label.hashCode() : 0)
            + super.hashCode();
    return hash;
}
}
2. Create a simple renderer to display your new list
public class NodeTreeViewRenderer extends TreeViewNodeRenderer
{

 @Override
 public void render(Object o, Path path, int i, TreeView tv, boolean bln,
         boolean bln1, NodeCheckState ncs, boolean bln2, boolean bln3)
 {
     if (o != null && o instanceof LabeledList)
     {
         super.render(((LabeledList) o).getLabel(), path, i, tv,
                 bln, bln1, ncs, bln2, bln3);
     } else
     {
         super.render(o, path, i, tv, bln, bln1, ncs, bln2, bln3);
     }
 }
}
3. Put it together.
@WTKX TreeView myTree;
...
...
public void startup(Display display,
          Map map) throws Exception
  {
      ....
      List rootList = new arrayList();
      rootList.add(new LabeledList(new Date()));
      myTree.setData(rootList);
  }
Now we have the problem of loading all the child nodes when the tree is expanded. This is handled by creating a TreeViewBranchListener and expanding LabeledList a little to track if it's been loaded or not. In LabeledList, add the following:
    private boolean loaded = false;

  public boolean isLoaded()
  {
      return loaded;
  }

  public void setLoaded(boolean loaded)
  {
      this.loaded = loaded;
  }
Now create the Listener
public class LazyLoadBranchListener implements TreeViewBranchListener
{
  public void branchExpanded(TreeView tv, Path path)
  {

      List currList = tv.getTreeData();

      for (Integer i : path)
      {
          currList = (List) currList.get(i);
      }

      LabeledList lList = (LabeledList) currList;

     if (!lList.isLoaded())
      {
          // load data for into lList. Add additional LabeledLists for directories or
          // straight objects for end nodes.
          // For long running loads, do the load in another thread
          lList.setLoaded(true);
      }
  }

  public void branchCollapsed(TreeView tv, Path path)
  {}
}
In your application class, add the new listener
    myTree.getTreeViewBranchListeners().add(new LazyLoadBranchListener());