Showing posts with label java. Show all posts
Showing posts with label java. Show all posts

Friday, 23 September 2011

Reading XML files in java

Example description: I have to create a custom group store so I could add them to the groups retrieved from LDAP. My choice is storing them in XML files. So I thought someone might find my experience interesting.

Prerequisites: Eclipse for java development with m2e plugin.

Create XSD file

Why? This will make creating XML files very easy and will ensure that those files will be readable as expected. I won't go into details, since it's not so hard to create even very complex schema definition files. But here's the result (file /src/main/resources/lv/rtu/itd/vivs/user-groups-1.0.xsd):

<schema elementformdefault="qualified" targetnamespace="http://vivs.rtu.lv/schemas/user-groups" xmlns:tns="http://vivs.rtu.lv/schemas/user-groups" xmlns="http://www.w3.org/2001/XMLSchema">

  <element name="user-groups" type="tns:user-groups-type">

  <complextype name="user-groups-type">
    <sequence>
      <element maxoccurs="unbounded" name="user-groups" type="tns:user-group" />
    </sequence>
  </complextype>

  <complextype name="user-group">
    <sequence>
      <element name="uid" type="string" />
      <element maxoccurs="unbounded" name="group" type="string" />
    </sequence>
  </complextype>

</element>

Configure class generation

Create file src/main/binding/bindings.xjb (this will instruct the generator how to create new classes):

<bindings xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
  xmlns:xs="http://www.w3.org/2001/XMLSchema" 
  xmlns="http://java.sun.com/xml/ns/jaxb"
  xmlns:xjc="http://java.sun.com/xml/ns/jaxb/xjc" 
  xsi:schemaLocation="http://java.sun.com/xml/ns/jaxb http://java.sun.com/xml/ns/jaxb/bindingschema_2_0.xsd"
  version="2.1">

  <bindings>
    <globalBindings>
      <serializable uid="1" />
      <xjc:simple />
    </globalBindings>
  </bindings>

  <bindings schemaLocation="../resources/lv/rtu/itd/vivs/user-groups-1.0.xsd">
    <schemaBindings>
      <package name="lv.rtu.itd.vivs.xml.user_groups" />
    </schemaBindings>
  </bindings>

</bindings>

Configure pom.xml and add something like this in order to tell Maven that it must now generate classes using jaxb2:

  <build>
    <plugins>
      <plugin>
        <groupId>org.jvnet.jaxb2.maven2</groupId>
        <artifactId>maven-jaxb2-plugin</artifactId>
        <version>0.7.4</version>
        <executions>
          <execution>
            <id>generate-xjc-sources</id>
            <phase>generate-sources</phase>
            <goals>
              <goal>generate</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <bindingDirectory>src/main/binding</bindingDirectory>
          <schemaDirectory>src/main/resources</schemaDirectory>
          <schemaIncludes>
            <include>lv/rtu/itd/vivs/*.xsd</include>
          </schemaIncludes>
          <extension>true</extension>
          <args>
            <arg>-no-header</arg>
            <arg>-readOnly</arg>
            <arg>-mark-generated</arg>
          </args>
        </configuration>
        <dependencies>
          <dependency>
            <groupId>com.sun.xml.bind</groupId>
            <artifactId>jaxb-xjc</artifactId>
            <version>2.2.1</version>
          </dependency>
        </dependencies>
      </plugin>
    </plugins>
  </build>

Now, probably, you must click on project and choose Maven > Update Project Configuration

Java code

Create interface for class:

package lv.rtu.itd.vivs.service;

import java.util.Set;

public interface GroupService {
    Set getGroups(String uid);
}

Create class that will unmarshal (read) the XML document:

package lv.rtu.itd.vivs.service.impl;

import java.io.IOException;
import java.io.InputStream;
import java.util.HashSet;
import java.util.Set;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;

import lv.rtu.itd.vivs.service.GroupService;
import lv.rtu.itd.vivs.xml.user_groups.UserGroup;
import lv.rtu.itd.vivs.xml.user_groups.UserGroups;

public class XmlGroupService implements GroupService {
    
    private Logger logger = LoggerFactory.getLogger(getClass());
    
    private UserGroups groups;
    
    public void setFilename(String filename) {
        JAXBContext ctx;
        try {
            ctx = JAXBContext.newInstance("lv.rtu.itd.vivs.xml.user_groups");
            Unmarshaller unmarshaller = ctx.createUnmarshaller();
            InputStream is = new ClassPathResource(filename).getInputStream();
            this.groups = (UserGroups) unmarshaller.unmarshal(is);
        } catch (JAXBException e) {
            logger.warn(e.getMessage(), e);
        } catch (IOException e) {
            logger.warn(e.getMessage(), e);
        }
    }
    
    @Override
    public Set getGroups(String uid) {
        for (UserGroup group : groups.getUserGroups()) {
            if (group.getUid().equals(uid)) {
                return new HashSet(group.getGroups());
            }
        }
        return new HashSet();
    }
}

Create test class (under src/test/java):

package lv.rtu.itd.vivs.service;

import java.util.HashSet;
import java.util.Set;

import lv.rtu.itd.vivs.service.impl.XmlGroupService;

import org.junit.Assert;
import org.junit.Test;

public class XmlGroupsTester {
    
    @Test()
    public void testGetGroups() {
        XmlGroupService service = new XmlGroupService();
        service.setFilename("user-groups.xml");
        
        Set expected = new HashSet();
        Assert.assertEquals(expected, service.getGroups("not-existing-user"));
        
        expected.add("my/demo/group");
        expected.add("my/demo/group2");
        expected.add("my/demo/group3");
        Assert.assertEquals(expected, service.getGroups("user.demo"));
    }
}

Now create XML file (src/test/resources/user-groups.xml:

<?xml version="1.0" encoding="UTF-8"?>
<user-groups xmlns="http://vivs.rtu.lv/schemas/user-groups" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://vivs.rtu.lv/schemas/user-groups http\://vivs.rtu.lv/schemas/user-groups">
  <user-groups>
    <uid>user.demo</uid>
    <group>my/demo/group</group>
    <group>my/demo/group2</group>
    <group>my/demo/group3</group>
  </user-groups>
</user-groups>

Now you can run tests and check the results.

Monday, 14 March 2011

Hibernate and native SQL fetching

Today it took me about 3 hours to find a problem in my code that was caused by HHH-2831. Unfortunately Hibernate documentation is vague regarding 'return-join' elements. By reading comments of bug, i realized that i'm not the only one facing the same problem and in all cases it was intuition of developers which proved to be wrong.

I'll explain briefly the essence of problem. Consider two simple hibernate class mappings and named native SQL query:

  <class name="ObjectA" table="OBJECT_A">
<id column="ID" name="id">
<generator class="native" />
</id>
<property name="title" column="TITLE" type="string" length="255" not-null="true" />
<list name="children" cascade="all-delete-orphan">
<key column="OBJECT_A_ID" not-null="true" />
<list-index column="ORDER_INDEX" base="0" />
<one-to-many class="ObjectB" />
</list>
</class>
<class name="ObjectB" table="OBJECT_B">
<id column="ID" name="id">
<generator class="native" />
</id>
<property name="title" column="TITLE" type="string" length="255" not-null="true" /> </class>
<sql-query name="getAll" read-only="true">
<return alias="a" class="com.example.ObjectA" />
<return-join alias="b" property="a.children" />
select {a.*}, {b.*}
from OBJECT_A a
inner join OBJECT_B b on b.OBJECT_A_ID = b.ID
</sql-query>

Now... what do you expect of when executing something like:

List result = session.getNamedQuery("getAll").list()

I was expecting it to be a list of objects of class ObjectA without any duplicates. Unfortunately this is not the case (like it would've been when using HQL). In fact it returns a list of Object[2] elements and with as many duplicates of ObjectA as it has children. The second object in array is ObjectB (child) object. Hence you must filter the list for duplicate elements:

        List finalResult = new ArrayList();
for (Object[] objs : result) {
ObjectA obj = (ObjectA) objs[0];
if (!finalResult.contains(obj)) {
finalResult.add(obj);
}
}

I wonder what will happen in case of another level... They cannot be filtered if those collections are not inverse, because it would automatically update the list of children (remove them).

Thursday, 16 September 2010

Retrieving password, using spring-ldap

Consider following code snippet:
public class PasswordContextMapper extends AbstractContextMapper {
public static final String UID = "uid";
public static final String PASSWORD = "userPassword";
public static final String[] LDAP_ATTRIBUTES = { UID, PASSWORD };

@Override
protected Password doMapFromContext(DirContextOperations ctx) {
final Password p = new Password();
p.setPassword(ctx.getStringAttribute(PASSWORD));
p.setUid(ctx.getStringAttribute(UID));
return p;
}
}

Looks ok? Think again. There's one pitfall with password retrieval from LDAP. Although different LDAP viewers show password as hashed text, it's really an array of bytes. This code throws something like
[B cannot be cast to java.lang.String
It actually says that "Cannot cast from array of bytes to String" ('[' shows that object is array of something). Solution is trivial. Change ctx.getStringAttribute(PASSWORD) to ctx.getObjectAttribute(PASSWORD).toString();

Wednesday, 11 March 2009

Java mail MIME type errors

I've developed small job on local machine which sends email with PDF attachments. But when i ran it on test environment, an exception was thrown:
javax.activation.UnsupportedDataTypeException: no object DCH for MIME type multipart/mixed;

While searching the solution, found this question. Solution i found was adding MIME type handlers programmatically:
// add handlers for main MIME types
MailcapCommandMap mc = (MailcapCommandMap)CommandMap.getDefaultCommandMap();
mc.addMailcap("text/html;; x-java-content-handler=com.sun.mail.handlers.text_html");
mc.addMailcap("text/xml;; x-java-content-handler=com.sun.mail.handlers.text_xml");
mc.addMailcap("text/plain;; x-java-content-handler=com.sun.mail.handlers.text_plain");
mc.addMailcap("multipart/*;; x-java-content-handler=com.sun.mail.handlers.multipart_mixed");
mc.addMailcap("message/rfc822;; x-java-content-handler=com.sun.mail.handlers.message_rfc822");
CommandMap.setDefaultCommandMap(mc);
Others solutions didn't work for me - i had recent activation.jar and mail.jar in WEB-INF/lib folder already. Can someone explain me why META-INF/mailcap file wasn't read from mail.jar?