пятница, 11 ноября 2011 г.

Чтение и запись Tab Separated Values файлов

Что же представляют из себя файлы в формате TSV? Это текстовые файлы, представляющие собой набор строк. В каждой строке одинаковое количество "полей" определенного формата, разделенные символом табуляции ("\t"). Похоже на некое подобие таблицы.

Итак, приступим к написанию reader-а:
/**
 * @author vie
 *
 * TSVReader.java
 * Class for read tab separated values files
 */
public class TSVReader<T> {

    private final String filePath;

    public TSVReader(String filePath) {
        this.filePath = filePath;
    }

    /**
     * This method reads tab separated values file
     * into object of the arbitrary class
     *
     * @param clazz type of transmitted class
     * @return List
     * @throws com.site.tsv.exceptions.TSVException
     */
    public List<T> read(Class<T> clazz) throws TSVException {
        List<T> result = new LinkedList<T>();

        try {
            BufferedReader bReader = new BufferedReader(new FileReader(filePath));
            String line;

            //recieving all lines
            int i = 1;
            while ((line = bReader.readLine()) != null) {
                //recieving all line members
                String datavalue[] = line.split("\t");

                //create instance of line entity
                Constructor constructor = clazz.getConstructor(null);
                Object somethingInstance = constructor.newInstance(null);

                //parsing line entity members
                Field[] fields = clazz.getDeclaredFields();

                //recieve map of line entity member types
                Map<Integer, Class<?>> mapFields = defineFields(clazz);

                //for each line entity field
                if (fields != null && datavalue != null && fields.length == datavalue.length) {
                    for (int j = 0; j < fields.length; j++) {
                        //for each line entity field
                        String s = fields[j].getName();
                        s = s.substring(0, 1).toUpperCase() + s.substring(1, s.length());

                        //exec line entity setter methods
                        Method method = clazz.getMethod("set" + s, mapFields.get(j));
                        method.invoke(somethingInstance, getDeterminatedValue(datavalue[j], fields[j].getType()));
                    }
                } else if (fields != null && datavalue != null && fields.length != datavalue.length) {
                    throw new TSVInvalidFileException("Count of formatted file fields on line " + i + " not equal count of entity fields");
                }

                i++;
                result.add((T) somethingInstance);
            }
            bReader.close();
        } catch (FileNotFoundException e) {
            throw new TSVException(e);
        } catch (IOException e) {
            throw new TSVException(e);
        } catch (ParseException e) {
            throw new TSVInvalidFileException(e);
        } catch (NumberFormatException e) {
            throw new TSVInvalidFileException(e);
        } catch (NoSuchMethodException e) { //reflect
            throw new TSVException(e);
        } catch (InstantiationException e) {
            throw new TSVException(e);
        } catch (IllegalAccessException e) {
            throw new TSVException(e);
        } catch (InvocationTargetException e) {
            throw new TSVException(e);
        }
        return result;
    }

    /**
     * Returns field names and types
     *
     * @param clazz type of transmitted class
     * @return Map<string,integer>.
     * @throws com.site.tsv.exceptions.TSVIncorrectAnnotatedFieldsException
     */
    private Map<Integer, Class<?>> defineFields(Class<?> clazz) throws TSVIncorrectAnnotatedFieldsException {
        Map<Integer, Class<?>> map = new HashMap<Integer, Class<?>>();

        Field[] fields = clazz.getDeclaredFields();
        int i = 0;
        for (Field field : fields) {
            if (field.isAnnotationPresent(TSVInteger.class)) {
                map.put(i++, Integer.class);
            } else if (field.isAnnotationPresent(TSVDouble.class)) {
                map.put(i++, Double.class);
            } else if (field.isAnnotationPresent(TSVString.class)) {
                map.put(i++, String.class);
            } else if (field.isAnnotationPresent(TSVDate.class)) {
                map.put(i++, Date.class);
            }
        }

        if (fields.length == 0 || fields.length != map.size()) {
            throw new TSVIncorrectAnnotatedFieldsException("Some field not annotated or count of fields = 0!");
        }

        return map;
    }

    /**
     * Returns converted to the native type field
     *
     * @param original value of obtained field
     * @param needType type of transmitted class
     * @return Object
     * @throws java.text.ParseException
     */
    private Object getDeterminatedValue(String original, Class<?> needType) throws ParseException {
        if (needType.getName().equals("java.lang.Integer")) {
            return Integer.parseInt(original);
        } else if (needType.getName().equals("java.lang.Double")) {
            return Double.parseDouble(original);
        } else if (needType.getName().equals("java.lang.String")) {
            return original;
        } else if (needType.getName().equals("java.util.Date")) {
            SimpleDateFormat formatter = new SimpleDateFormat("dd.MM.yyyy");
            return formatter.parse(original);
        } else {
            return null;
        }
    }
}
В классе я использовал generics, чтобы проверять заданного типы entity на этапе компиляции. В преобразовании строк с различными типами данных в entity объект мне помог reflection. Для разбора с его помощью entity, написаны приведенные ниже аннотации. Также я создал несколько исключений, чтобы в при выпадении ошибок можно было быстро понять что свалилось, будь то reflection, IO или преобразование чисел и дат.

среда, 9 ноября 2011 г.

Разделение выполнения задачи на n потоков

Бывает при решении некоторых задач, таких как загрузка данных с FTP приходится разделять ход выполнения задачи на несколько потоков.
Казалось бы, все просто. Создаем n-е количество потоков (каждый из которых делает часть общей работы) и запускаем их:
public class MainClass {
  public static void main(String[] args) {
    WorkClass work = new WorkClass();
    work.doSomeWork();
  }
}

public class WorkClass {

  public void doSomeWork() {             
    for (int i = 0; i < 5; i++) { 
      Worker worker = new Worker("directory" + i);
      Thread thread = new Thread(worker);
      thread.start();
    }
    
    //here we must wait for execution of all threads

    //doing work on    
  }

  public class Worker implements Runnable {

    private String dir;

    public Worker(String dir) {
      this.dir = dir;
    }
  
    @Override
    public void run() {
      doPieceOfWork();
    }

    private void doPieceOfWork() {
      //doing loading from FTP, depending on the name of directory ...
    }
  }
}
Здесь в качестве примера разделения выполняемой работы на 5 потоков приводится загрузка по FTP. Каждый поток грузит свою папку. Но может возникнуть проблема. Надо подождать, пока выполнятся все запущенные потоки, и только после этого продолжать работу основного потока. Для этого обернем вызов потоков в блок синхронизации.
public void doSomeWork() {
  synchronized (this) {             
    for (int i = 0; i < 5; i++) { 
      Worker worker = new Worker("directory" + i);
      Thread thread = new Thread(worker);
      thread.start();
    }
  }
}
Далее остановим главный поток после запуска второстепенных потоков до тех пор, пока не получим ответ о завершении работы каждого из них. Для этого сделаем цикл, в котором получаем уведомление от одного из потоков и, если количество завершенных второстепенных потоков меньше количества запущенных, продолжаем ожидать ответа от оставшихся:
public void doSomeWork() {
  synchronized (this) { 
    Vector events = new Vector();
            
    for (int i = 0; i < 5; i++) {       
      Worker worker = new Worker(this, "directory" + i, events);
      Thread thread = new Thread(worker);
      thread.start();
    }

    //if all threads are dead, go ahead
    while (true) {
      try {
        this.wait();
      } catch (InterruptedException e) {
        errorLogger.error(e.getMessage(), e);
      }
      if (events.size() == 5) {
        break;
      }
    }        
  }

  //doing work on
}
теперь надо добавить во второстепенные потоки уведомления, которые будет получать основной поток:
public class Worker implements Runnable {

  private String dir;

  private Vector events;

  public Worker(WorkClass monitor, String dir, Vector events) {
    this.dir = dir;
    this.events = events;
  }
  
  @Override
  public void run() {
    doPieceOfWork();
  }

  private void doPieceOfWork() {
    //doing loading from FTP, depending on the name of directory ...
    
    synchronized (monitor) {
      events.add(new Object());
      monitor.notify();      
    }
  }
}
Cледует обратить внимание, что блоки синхронизации во второстепенных потоках и в главном создаются по монитору work класса WorkClass. Также в класс Worker добавлена коллекция events, которая выполняет роль счетчика завершенных потоков. Класс Vector выбран не случайно, тк его методы синхронизированы. В качестве его альтернативы можно воспользоваться Collections.synchronizedList(new ArrayList()). Хотя в данном случае синхронизация методов коллекции и не будет играть никакой роли, тк работа с ней происходит в блоке синхронизации. Все, теперь метод doSomeWork() разделит загрузку по FTP на 5 потоков и продолжит свою работу после завершения всех запущенных потоков. Общий результат выглядит так:
public class MainClass {
  public static void main(String[] args) {
    WorkClass work = new WorkClass();
    work.doSomeWork();
  }
}

public class WorkClass {
  protected static final Logger errorLogger = LoggerFactory.getLogger("errorLogger");

  public void doSomeWork() {
    synchronized (this) {     
      Vector events = new Vector();
        
      for (int i = 0; i < 5; i++) { 
        Worker worker = new Worker(this, "directory" + i, events);
        Thread thread = new Thread(worker);
        thread.start();
      }
      
      //if all threads are dead, go ahead
      while (true) {
        try {
          this.wait();
        } catch (InterruptedException e) {
          errorLogger.error(e.getMessage(), e);
        }
        if (events.size() == 5) {
          break;
        }
      }
    }
    
    //doing work on 
  }

  public class Worker implements Runnable {

    private String dir;

    private Vector events;

    public Worker(WorkClass monitor, String dir, Vector events) {
      this.dir = dir;
      this.events = events;
    }
  
    @Override
    public void run() {
      doPieceOfWork();
    }

    private void doPieceOfWork() {
      //doing loading from FTP, depending on the name of directory ...
    
      synchronized (monitor) {
        events.add(new Object());
        monitor.notify();        
      }
    }
  }
}

понедельник, 31 октября 2011 г.

Maven Shade Plugin

Shade плагин как альтернатива Assembly.

На днях используя Assembly, столкнулся со следующей проблемой. Приложение со Spring собрал в jar. При запуске валилась ошибка:
Exception in thread "main" org.springframework.beans.factory.parsing.BeanDefinitionParsingException:
Configuration problem: Unable to locate Spring NamespaceHandler
for XML schema namespace [http://www.springframework.org/schema/context]
at org.springframework.beans.factory.parsing.FailFastProblemReporter.
error(FailFastProblemReporter.java:68)
at org.springframework.beans.factory.parsing.ReaderContext.error(ReaderContext.java:85)
...

Это означает что при копировании в META-INF спринговых файлов spring.schemas и spring.handlers, они перезатираются для каждой spring библиотеки.

Assebly плагин выглядел так:

    maven-assembly-plugin
    2.2.1
    
        false
        
            jar-with-dependencies
        
        
            
                com.mypackage.MainClass
            
        
    


Немного погуглив, нашел вариант с обьявлением своего assembly дескриптора, или со скачиванем all-in-one Spring библиотеки. Но на stackoverflow посоветовали использовать плагин Shade.

Обьявление Shade:

    org.apache.maven.plugins
    maven-shade-plugin
    
        
            package
            
                shade
            
            
                
                    
                        com.mypackage.MainClass
                    
                    
                        META-INF/spring.handlers
                    
                    
                        META-INF/spring.schemas
                    
                
                
                    
                        *:*
                        
                            META-INF/*.SF
                            META-INF/*.DSA
                            META-INF/*.RSA
                        
                    
                
            
        
    


Все. После этого спокойно собираем jar командой package и никаких проблем.

среда, 28 сентября 2011 г.

RESTful веб-сервис на Jersey + Spring 3

Пишем простой REST веб-сервис.

Как известно, начиная с третьей версии у Spring появилась возможность легко создавать REST веб сервис с помощью Spring MVC (на аннотациях @RequestMapping). Но я хочу рассмотреть классический Jax-RS с помощью Jersey. Итак, начнем.

Для начала определим maven зависимости:




    com.sun.jersey
    jersey-server
    1.9.1




    com.sun.jersey.contribs
    jersey-spring
    1.9.1
    
        
            org.springframework
            spring
        
        
            org.springframework
            spring-core
        
        
    



    com.sun.xml.bind
    jaxb-impl
    2.1.9

exclusions в зависимости jersey+spring необходимы потому, что библиотека требует зависимости спринга версий 2 или 2.5. А мы ей их дать не можем, тк используем третью версию. В исключениях требуется указать все библиотеки Spring, которые у нас есть в проекте.

Теперь можно писать сам сервис. Будем доставать продукты по их id:
@Path("/product/{id}")
@Component
@Scope("prototype")
public class ProductRSController {

    @Autowired
    private ProductService srv;

    @GET
    @Produces(MediaType.APPLICATION_XML)
    public Product getDescription(@PathParam("id") String productId) {
        Product product = srv.getProduct(productId);
        return product;
    }
}
Пометив метод с помощью аннотаций @GET и @Produces(MediaType.APPLICATION_XML), мы указали, что url вида http://myserver.com/myapp/rs/product/123 (где myserver.com - наш сервер, myapp - имя развернутого приложения), будет возвращать в ответ сериализованный обьект Product в xml.

Класс ProductService представляет собой сервис бин, с методом Product getProduct(String productId) {...}

Не забудем настроить web.xml:

    contextConfigLocation/WEB-INF/spring-context.xml


    org.springframework.web.context.ContextLoaderListener



    Jersey Web Application
    com.sun.jersey.spi.spring.container.servlet.SpringServlet
    1



    Jersey Web Application
    /rs/*

и spring-context.xml:




Теперь можно разворачивать приложение и зайти по URL http://myserver.com/myapp/rs/product/123. В случае успеха мы увидим полученный XML, если конечно у нас есть Product с id = 123.

Теперь можно создавать клиент веб-сервиса. Для удобства работы, возьмем клиент от Jersey.

зависимости:

    com.sun.xml.bind
    jaxb-impl
    2.1.9



    com.sun.jersey
    jersey-client
    1.9.1


сам клиент будет выглядеть так:
public class RESTClient {

    public Product recieveObject() {
        Client c = Client.create();
        WebResource r = client.resource("http://myserver.com/myapp/rs/");
        Product product = response = r.path("product/123").
                accept(MediaType.APPLICATION_XML_TYPE).
                get(Product.class);

        return product;
    }
    
    public String recieveXML() {
        Client c = Client.create();
        WebResource r = client.resource("http://myserver.com/myapp/rs/");
        String product = response = r.path("product/123").
                accept(MediaType.APPLICATION_XML_TYPE).
                get(String.class);

        return product;
    }
}
метод recieveObject() возвращает десериализованный обьект Product, а метод recieveXML() возвращает XML.

С помощью Generics можно сделать клиент универсальным:
public class RESTClient {

    private static final Client client;

    private String resource = "http://myserver.com/myapp/rs/";

    static {
        client = Client.create();
    }

    public RESTClient() {
    }

    public RESTClient(String resource) {
        this.resource = resource;
    }

    public void recieve() {

        //object
        Product product = recieve(Product.class, "product/", "123");
        System.out.println("response.getProductId: " + product.getProductId());
        
        //string 
        String xml = recieve(String.class, "product/", "123");
        System.out.println("response: " + xml);
    }

    private <T> T recieve(Class<T> type, String URI, String id) {
        WebResource r = client.resource(resource);

        T response = r.path(URI + id).
                accept(MediaType.APPLICATION_XML_TYPE).
                get(type);

        return response;
    }
}


Для настройки вывода данных в формате JSON необходимо проделать следующее:

Добавить зависимость:

            com.sun.jersey
            jersey-json
            1.10
        

Создать класс JAXBContextResolver в том же package, где лежит контроллер
@Provider
public final class JAXBContextResolver implements ContextResolver<jaxbcontext> {

    private final JAXBContext context;

    private final Set<class> types;

    private final Class[] cTypes = {MyEntity1.class, MyEntity1.class};

    public JAXBContextResolver() throws Exception {
        this.types = new HashSet(Arrays.asList(cTypes));
        this.context = new JSONJAXBContext(JSONConfiguration.natural().build(), cTypes);
    }

    @Override
    public JAXBContext getContext(Class<?> objectType) {
        return (types.contains(objectType)) ? context : null;
    }
}
где MyEntity1, MyEntity2 - отображаемые в JSON обьекты


и в контроллере указывать формат данных @Produces("application/json; charset=utf-8")

среда, 7 сентября 2011 г.

Scala под windows 7. ошибка при запуске компилятора.

После установки Scala 2.9.0 на win 7 столкнулся с проблемой. (С предыдущими версиям scala все нормально)

при запуске команды scalac -version неожиданно вываливается Эксепшн:

Exception in thread "main" java.lang.NoClassDefFoundError: scala/tools/nsc/MainGenericRunner
Caused by: java.lang.ClassNotFoundException: scala.tools.nsc.MainGenericRunner
        at java.net.URLClassLoader$1.run(URLClassLoader.java:202)
        at java.security.AccessController.doPrivileged(Native Method)
        at java.net.URLClassLoader.findClass(URLClassLoader.java:190)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:307)
        at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:301)
        at java.lang.ClassLoader.loadClass(ClassLoader.java:248)
Could not find the main class: scala.tools.nsc.MainGenericRunner.  Program will exit.

Начал разбираться в чем проблема. Оказалось, что при запуске scala.bat тоже самое.

Путем трейса файлов scalac.bat и scala.bat установил переменная _SCALA_HOME
в них неверная:
_SCALA_HOME c:\Program Files\Files\scala\bin\..

Хотя Переменная среды проставлена правильно:
c:\Program Files\scala\bin\..

В итоге пришлось в файлах scala.bat и scala.sh исправить строчки:

:set_home
  set _BIN_DIR=
  rem for %%i in (%~sf0) do set _BIN_DIR=%_BIN_DIR%%%~dpsi
  rem set _SCALA_HOME=%_BIN_DIR%..
  set _SCALA_HOME=%~dps0..
goto :eof

здесь заккоментировал:
for %%i in (%~sf0) do set _BIN_DIR=%_BIN_DIR%%%~dpsi
set _SCALA_HOME=%_BIN_DIR%..

и добавил
set _SCALA_HOME=%~dps0..

суббота, 27 августа 2011 г.

AspectJ + Maven + Spring + IDEA

Как заставить работать AspectJ с Maven и Spring.

Подцепим зависимости maven:

    org.springframework
    spring-aop
    ${spring.version}
    
        
            org.aspectj
            aspectjweaver
        
    



    org.springframework
    spring-aspects
    ${spring.version}
    
        
            org.aspectj
            aspectjweaver
        
    





    org.aspectj
    aspectjrt
    ${aspectj.version}


Для выполнения aspectj компиляции добавим плагин в maven:

    org.codehaus.mojo
    aspectj-maven-plugin
    1.1
    
        1.6
        1.6
        true
        ignore
        
            
                org.springframework
                spring-aspects
            
        
        false
    
    
        
            
                compile
                test-compile
            
        
    


Все, проект можно собирать.
Для выполнения приложения прямо из IDEA надо поставить галку "Run Maven Goal" в Run -> Edit Configurations -> Before Launch и выбрать в ней плагин aspectj:compile

суббота, 9 июля 2011 г.

Singleton и Prototype scope в Spring

Singleton scope означает что экземпляр бина создается один раз, при инициализации контекста. Не стоит сравнивать этот scope со stateless bean в EJB. EJB контейнер при каждом обращении выдает новый экземпляр бина. Поэтому в stateless бине состояние не сохраняется. Таким образом Singleton scope в Spring - тоже statefull. В случае со scope Prototype, можно создавать его новые экземпляры, когда это надо, с помощью фабрики. (statefull)

В обьявлении этих бинов нет ничего сложного:



Но проблема появляется, когда нужна зависимость SingletonBean от PrototypeBean. В этом случае можно поступить несколькими способами:
ApplicationContextAware или BeanNameAware method injection; Lookup method injection; Arbitrary method replacement.
Расскажу о самом простом (по крайней мере для меня) из них - аналоге BeanNameAware.

PrototypeBean может представлять из себя что угодно. В данном контексте нам это не важно, так что будем рассматривать его просто как POJO класс:
public class PrototypeBean {
...
}

а в SingletonBean используем фабрику org.springframework.beans.factory.ObjectFactory:
public class SingletonBean {

  private ObjectFactory factory;

  public void createPrototypeBean() {
     //здесь можем получить сколько угодно экземпляров PrototypeBean
     PrototypeBean bean = factory.getObject();
  }

  ...

  public void setFactory(ObjectFactory beanFactory) throws BeansException {
     this.factory = beanFactory;
  }
}

Чтобы явно указать ObjectFactory экземпляр какого бина создавать, в конфигурации пропишем ObjectFactory как property Singleton-бина: