28 November, 2013

Spring-boot JPA sample with auto-configured repository

Spring-boot allows to create a completely minimalistic applications.
For example, its auto-configuration ability raises and configures JPA and Hibernate-related stuff:
DataSource, Entity and Transaction Managers, Repositories.
With no manual configuration in your code or in xml files.

And the application code is getting smaller and smaller...

User.java
package sb; 
 
import javax.persistence.*; 
 
@Entity 
@Table(name = "sb_user") 
public class User { 
 
    @Id 
    @GeneratedValue(strategy = GenerationType.AUTO) 
    public long id; 
    public String name; 
    public String alias; 
 
    public User() { 
    } 
 
    public User(long id, String name) {
        this.id = id; 
        this.name = name; 
    } 
} 
UserRepository.java
package sb;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;

import java.util.List;

public interface UserRepository extends JpaRepository<User, Long> {

    @Query("SELECT u FROM User u WHERE u.alias IS NOT NULL")
    List<User> findAliased();
} 
UserController.java
package sb;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("user")
public class UserController {
    protected final Logger log = LoggerFactory.getLogger(getClass());

    @Autowired
    private UserRepository users;

    @RequestMapping("test")
    public String test() {
        log.info("Test");
        return "OK";
    }

    @RequestMapping("user")
    public User getUser(@RequestParam("id") long id) {
        log.info("Get user");
        return users.findOne(id);
    }

    @RequestMapping("users")
    public List<User> getUsers(@RequestParam("ids") List<Long> ids) {
    log.info("Get users");
        return users.findAll(ids);
    }

    @RequestMapping("aliased")
    public List<User> getAliasedUsers() {
    log.info("Get aliased users");
        return users.findAliased();
    }
} 
Application.java
package sb;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@EnableAutoConfiguration
@Configuration
@ComponentScan
public class Application {

    public static void main(String[] args) throws Throwable {
        new SpringApplication(Application.class).run(args);
    }
} 
application.properties
spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://srv0/test
spring.datasource.username=test
spring.datasource.password=test 
schema.sql
-- DROP TABLE sb_user;

CREATE TABLE sb_user
(
  id   SERIAL PRIMARY KEY    NOT NULL,
  name CHARACTER VARYING(65) NOT NULL,
  alias CHARACTER VARYING(20)
);

INSERT INTO sb_user (name) VALUES ('Ben Linus');
INSERT INTO sb_user (name) VALUES ('Claire Littleton');
INSERT INTO sb_user (name) VALUES ('Desmond Hume');
INSERT INTO sb_user (name) VALUES ('Frank Lapidus');
INSERT INTO sb_user (name, alias) VALUES ('Hugo Reyes', 'Hurley');
INSERT INTO sb_user (name) VALUES ('Jack Shephard');
INSERT INTO sb_user (name, alias) VALUES ('James Ford', 'Sawyer');
INSERT INTO sb_user (name) VALUES ('Jin-Soo Kwon');
INSERT INTO sb_user (name) VALUES ('Miles Straume');
INSERT INTO sb_user (name) VALUES ('Richard Alpert');
INSERT INTO sb_user (name) VALUES ('Sayid Jarrah');
INSERT INTO sb_user (name) VALUES ('Sun-Hwa Kwon');



Build and run command: gradle clean build runJar

Test urls:
http://localhost:8080/user/user?id=2
http://localhost:8080/user/users?ids=1,3,5,7
http://localhost:8080/user/aliased

26 November, 2013

Spring-boot memory usage

Typical spring-boot application - single jar file.
Size of the file is about 10+ megabytes (it contains another jars, classes and resources inside).
What about memory usage for this app?
I created sample with only one web @Controller and two webmethods: first - reports memory usage, second - runs Java Garbage Collector.

Memory usage after start (http://localhost:8080/test/mem):
max: 1,875,378,176 bytes
total: 355,729,408 bytes
free:  178,138,976 bytes
used:  177,590,432 bytes
and after GC (http://localhost:8080/test/rungc):
max: 1,875,378,176 bytes
total: 355,729,408 bytes
free:  331,821,496 bytes
used:   23,907,912 bytes
With spring-boot we have a lot of available memory)

Controller.java
package sb; 
 
import org.springframework.web.bind.annotation.RequestMapping; 
import org.springframework.web.bind.annotation.RestController; 
 
@RestController 
@RequestMapping("test") 
public class Controller { 
 
    @RequestMapping(value = "mem", produces = "text/plain")
    public String mem() { 
        long max = Runtime.getRuntime().maxMemory(); 
        long total = Runtime.getRuntime().totalMemory(); 
        long free = Runtime.getRuntime().freeMemory(); 
        long used = total - free; 
        return String.format("Memory:\n" +
                "max: %,d bytes\n" +
                "total: %,d bytes\n" +
                "free: %,d bytes\n" +
                "used: %,d bytes\n",
                max, total, free, used); 
    } 
 
    @RequestMapping("rungc") 
    public String rungc() { 
        Runtime.getRuntime().gc(); 
        return "Run GC";
    } 
} 


Application.java
package sb; 
 
import org.springframework.boot.SpringApplication; 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 
import org.springframework.context.annotation.ComponentScan; 
import org.springframework.context.annotation.Configuration; 
 
@EnableAutoConfiguration 
@Configuration 
@ComponentScan 
public class Application { 
 
    public static void main(String[] args) throws Throwable {
        new SpringApplication(Application.class).run();
    } 
} 

build.gradle
buildscript { 
    repositories { 
        maven { url "http://repo.spring.io/libs-snapshot" }
    } 
    dependencies { 
        classpath("org.springframework.boot:spring-boot-gradle-plugin:0.5.0.M6")
    } 
} 
 
apply plugin: "java" 
apply plugin: "spring-boot" 
 
buildDir = "out" 
 
jar { 
    baseName = "sb-mem" 
    version = "0.1" 
} 
 
repositories { 
    mavenCentral() 
    maven { url "http://repo.spring.io/libs-snapshot" }
} 
 
dependencies { 
    def springBootVersion = '0.5.0.M6'
    compile("org.springframework.boot:spring-boot-starter-web:$springBootVersion")
} 

25 November, 2013

Spring-boot change log-file location

By default spring-boot writes logs to the /tmp/spring.log file.
But if you need another location - just set LOG_FILE variable in logback.xml configuration.

logback.xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
 
    <property name="LOG_FILE" value="tmp/application.log"/>
 
    <include resource="org/springframework/boot/logging/logback/base.xml"/>
 
    <logger name="sb" level="DEBUG"/>
 
</configuration> 

Application.java
package sb;
 
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.SpringApplication;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Service;
 
@Configuration 
public class Application {
 
    @Service 
    static class TestBean {
        protected final Logger log = LoggerFactory.getLogger(getClass());
 
        void test() {
            log.debug("TestBean.test");
        } 
    } 
 
 
    public static void main(String[] args) throws Throwable {
        SpringApplication app = new SpringApplication(Application.class);
        ConfigurableApplicationContext ctx = app.run(args); 
 
        TestBean bean = ctx.getBean(TestBean.class);
        bean.test(); 
    } 
} 

build.gradle
buildscript {
    repositories { 
        maven { url "http://repo.spring.io/libs-snapshot" }
    } 
    dependencies { 
        classpath("org.springframework.boot:spring-boot-gradle-plugin:0.5.0.M6")
    } 
} 
 
apply plugin: "java"
apply plugin: "spring-boot"
 
buildDir = "out"
 
jar { 
    baseName = "sb-log-file"
    version = "0.1"
} 
 
repositories { 
    mavenCentral() 
    maven { url "http://repo.spring.io/libs-snapshot" }
} 
 
dependencies { 
    def springBootVersion = '0.5.0.M6'
    compile("org.springframework.boot:spring-boot-starter:$springBootVersion")
}

21 November, 2013

Spring-boot JDBC with multiple DataSources sample

Spring-Boot's auto-configurer seems good for simple applications. For example it automatically creates DataSource and JdbcTemplate, when you need to connect to the database. But what about less trivial configuration, when you have several different databases?

I found the way to have multiple DataSources for Spring-Boot-based application.

In the sample below I have two special (db-related) Configurations, one properties file with connections' parameters and two Repositories.

Each @Repository connects with appropriate database through separate DataSource.
application.properties
spring.ds_items.driverClassName=org.postgresql.Driver 
spring.ds_items.url=jdbc:postgresql://srv0/test 
spring.ds_items.username=test0 
spring.ds_items.password=test0 
 
 
spring.ds_users.driverClassName=org.postgresql.Driver 
spring.ds_users.url=jdbc:postgresql://srv1/test 
spring.ds_users.username=test1 
spring.ds_users.password=test1 



DatabaseItemsConfig.java
package sb; 
 
import org.springframework.boot.autoconfigure.jdbc.TomcatDataSourceConfiguration; 
import org.springframework.boot.context.properties.ConfigurationProperties; 
import org.springframework.context.annotation.Bean; 
import org.springframework.context.annotation.Configuration; 
import org.springframework.jdbc.core.JdbcTemplate; 
 
import javax.sql.DataSource; 
 
@Configuration 
@ConfigurationProperties(name = "spring.ds_items") 
public class DatabaseItemsConfig extends TomcatDataSourceConfiguration { 
 
    @Bean(name = "dsItems") 
    public DataSource dataSource() { 
        return super.dataSource(); 
    } 
 
    @Bean(name = "jdbcItems") 
    public JdbcTemplate jdbcTemplate(DataSource dsItems) { 
        return new JdbcTemplate(dsItems); 
    } 
} 


DatabaseUsersConfig.java
package sb; 
 
import org.springframework.boot.autoconfigure.jdbc.TomcatDataSourceConfiguration; 
import org.springframework.boot.context.properties.ConfigurationProperties; 
import org.springframework.context.annotation.Bean; 
import org.springframework.context.annotation.Configuration; 
import org.springframework.jdbc.core.JdbcTemplate; 
 
import javax.sql.DataSource; 
 
@Configuration 
@ConfigurationProperties(name = "spring.ds_users") 
public class DatabaseUsersConfig extends TomcatDataSourceConfiguration { 
 
    @Bean(name = "dsUsers") 
    public DataSource dataSource() { 
        return super.dataSource(); 
    } 
 
    @Bean(name = "jdbcUsers") 
    public JdbcTemplate jdbcTemplate(DataSource dsUsers) { 
        return new JdbcTemplate(dsUsers); 
    } 
 
} 


ItemRepository.java
package sb; 
 
import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.beans.factory.annotation.Qualifier; 
import org.springframework.jdbc.core.JdbcTemplate; 
import org.springframework.jdbc.core.RowMapper; 
import org.springframework.stereotype.Repository; 
 
import java.sql.ResultSet; 
import java.sql.SQLException; 
 
@Repository 
public class ItemRepository { 
    protected final Logger log = LoggerFactory.getLogger(getClass()); 
 
    @Autowired 
    @Qualifier("jdbcItems") 
    protected JdbcTemplate jdbc; 
 
    public Item getItem(long id) { 
        return jdbc.queryForObject("SELECT * FROM sb_item WHERE id=?", itemMapper, id); 
    } 
 
    private static final RowMapper<Item> itemMapper = new RowMapper<Item>() {
        public Item mapRow(ResultSet rs, int rowNum) throws SQLException { 
            Item item = new Item(rs.getLong("id"), rs.getString("title")); 
            item.price = rs.getDouble("id"); 
            return item; 
        } 
    }; 
} 


UserRepository.java
package sb; 
 
import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.beans.factory.annotation.Qualifier; 
import org.springframework.jdbc.core.JdbcTemplate; 
import org.springframework.jdbc.core.RowMapper; 
import org.springframework.stereotype.Repository; 
 
import java.sql.ResultSet; 
import java.sql.SQLException; 
 
@Repository 
public class UserRepository { 
    protected final Logger log = LoggerFactory.getLogger(getClass()); 
 
    @Autowired 
    @Qualifier("jdbcUsers") 
    protected JdbcTemplate jdbc; 
 
    public User getUser(long id) { 
        return jdbc.queryForObject("SELECT * FROM sb_user WHERE id=?", userMapper, id); 
    } 
 
    private static final RowMapper<User> userMapper = new RowMapper<User>() {
        public User mapRow(ResultSet rs, int rowNum) throws SQLException { 
            User user = new User(rs.getLong("id"), rs.getString("name")); 
            user.alias = rs.getString("alias"); 
            return user; 
        } 
    }; 
} 


Controller.java
package sb; 
 
import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
import org.springframework.beans.factory.annotation.Autowired; 
import org.springframework.web.bind.annotation.RequestMapping; 
import org.springframework.web.bind.annotation.RequestParam; 
import org.springframework.web.bind.annotation.RestController; 
 
@RestController 
public class Controller { 
    protected final Logger log = LoggerFactory.getLogger(getClass()); 
 
    @Autowired 
    private UserRepository users; 
 
    @Autowired 
    private ItemRepository items; 
 
    @RequestMapping("test") 
    public String test() { 
        log.info("Test"); 
        return "OK"; 
    } 
 
    @RequestMapping("user") 
    public User getUser(@RequestParam("id") long id) { 
        log.info("Get user"); 
        return users.getUser(id); 
    } 
 
    @RequestMapping("item") 
    public Item getItem(@RequestParam("id") long id) { 
        log.info("Get item"); 
        return items.getItem(id); 
    } 
 
} 


Application.java
package sb; 
 
import org.springframework.boot.SpringApplication; 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration; 
import org.springframework.context.annotation.ComponentScan; 
import org.springframework.context.annotation.Configuration; 
 
@EnableAutoConfiguration(exclude = DataSourceAutoConfiguration.class) 
@Configuration 
@ComponentScan(basePackages = "sb") 
public class Application { 
 
    public static void main(String[] args) throws Throwable { 
        SpringApplication app = new SpringApplication(Application.class); 
        app.run(); 
    } 
} 

20 November, 2013

Spring-boot JDBC sample

Simple Web+JDBC application.

With spring-boot you just do your business. Without overhead, without extra manipulations and configurations.
In example below:
  • UserRepository provides information about users from database via JdbcTemplate, plain SQL-queries and RowMapper.
  • UserController makes them accessible via HTTP.
In this example spring-boot automatically configures DataSource and JdbcTemplate according to application.properties file.

Sample urls:
  • http://localhost:8080/user/test
  • http://localhost:8080/user/user?id=2
  • http://localhost:8080/user/users?ids=1,3,5,7

Sources are minimalistic:
Application.java
package sb; 
 
import org.springframework.boot.SpringApplication; 
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; 
import org.springframework.context.annotation.ComponentScan; 
import org.springframework.context.annotation.Configuration; 
 
@EnableAutoConfiguration 
@Configuration 
@ComponentScan 
public class Application { 
 
    public static void main(String[] args) throws Throwable { 
        SpringApplication app = new SpringApplication(Application.class); 
        app.setShowBanner(false); 
        app.run(args); 
    } 
} 
User.java
package sb;

public class User {
    public long id;
    public String name;
    public String alias;

    public User(long id, String name) {
        this.id = id;
        this.name = name;
    }
} 
UserController.java
package sb;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@RestController
@RequestMapping("user")
public class UserController {
    protected final Logger log = LoggerFactory.getLogger(getClass());

    @Autowired
    private UserRepository users;

    @RequestMapping("test")
    public String test() {
        log.info("Test");
        return "OK";
    }

    @RequestMapping("user")
    public User getUser(@RequestParam("id") long id) {
        log.info("Get user");
        return users.getUser(id);
    }

    @RequestMapping("users")
    public List<User> getUsers(@RequestParam("ids") long[] ids) {
        log.info("Get users");
        return users.getUsers(ids);
    }
} 
UserRepository.java
package sb;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;

@Repository
public class UserRepository {
    protected final Logger log = LoggerFactory.getLogger(getClass());

    @Autowired
    protected JdbcTemplate jdbc;

    public User getUser(long id) {
        return jdbc.queryForObject("SELECT * FROM sb_user WHERE id=?", userMapper, id);
    }

    public List<User> getUsers(long[] ids) {
        String inIds = StringUtils.arrayToCommaDelimitedString(ObjectUtils.toObjectArray(ids));
        return jdbc.query("SELECT * FROM sb_user WHERE id IN (" + inIds + ")", userMapper);
    }

    private static final RowMapper<User> userMapper = new RowMapper<User>() {
        public User mapRow(ResultSet rs, int rowNum) throws SQLException {
            User user = new User(rs.getLong("id"), rs.getString("name"));
            user.alias = rs.getString("alias");
            return user;
        }
    };

} 
application.properties
spring.datasource.driverClassName=org.postgresql.Driver
spring.datasource.url=jdbc:postgresql://srv0/test
spring.datasource.username=test
spring.datasource.password=test 
build.gradle
buildscript { 
    repositories { 
        maven { url "http://repo.spring.io/libs-snapshot" } 
    } 
    dependencies { 
        classpath("org.springframework.boot:spring-boot-gradle-plugin:0.5.0.M6") 
    } 
} 
 
apply plugin: "java" 
apply plugin: "spring-boot" 
 
buildDir = "out" 
 
jar { 
    baseName = "sb-jdbc" 
    version = "0.1" 
} 
 
repositories { 
    mavenCentral() 
    maven { url "http://repo.spring.io/libs-snapshot" } 
} 
 
dependencies { 
    def springBootVersion = '0.5.0.M6' 
 
    compile("org.springframework.boot:spring-boot-starter-web:$springBootVersion") 
    compile("org.springframework.boot:spring-boot-starter-jdbc:$springBootVersion") 
 
    compile("org.postgresql:postgresql:9.2-1003-jdbc4") 
} 

Spring-boot

Spring's guys are doing new revolutionary thing - spring-boot
With spring-boot creation of standalone application has become much easier and faster.
Auto-configurations, embedded servlet-containers, all-in-one jar - sometimes it looks like magic.
It seems OK for starters and prototypes, but what about production and hi-load applications...

19 November, 2013

Java -XX: AutoBoxCacheMax option

-XX:AutoBoxCacheMax=size option lets to control cache size for Integers.

In current Oracle HotSpot JVM (1.7) it only affects on Integer objects, created via autoboxing or Integer.valueOf(int i).

public class IntegersDemo {
    public static void main(String[] args) {
        {
            Integer a100 = 100;
            Integer b100 = 100;
            System.out.println(a100 == b100); //true
 
            Integer a200 = 200;
            Integer b200 = 200;
            System.out.println(a200 == b200); //false by default, 
            // but true when -XX:AutoBoxCacheMax=1000
        }
 
        {
            Integer a100 = Integer.valueOf(100);
            Integer b100 = Integer.valueOf(100);
            System.out.println(a100 == b100); //true
 
            Integer a200 = Integer.valueOf(200);
            Integer b200 = Integer.valueOf(200);
            System.out.println(a200 == b200); //false by default,
            // but true when -XX:AutoBoxCacheMax=1000
        }
 
        {
            Integer a100 = new Integer(100);
            Integer b100 = new Integer(100);
            System.out.println(a100 == b100); //always false, 
            // because of objects are allocated via "new"
        }
    }
}
 

12 January, 2013

Amazon Server blog

I continue to write articles about Amazon Services and servers here: Amazon Server

07 January, 2013

Convert image to base64

It is extremely simple, and  without server-side:

<script>
    function convertToBase64(event) {
        var file = event.target.files[0];
        var reader = new FileReader();
        reader.onload = function (event) {
            document.getElementById('text').value = event.target.result;
            document.getElementById('text').select();
        };
        reader.readAsDataURL(file);
    }
</script>

<input id="file" type="file" onchange="convertToBase64(event)">
<textarea id="text"> </textarea>

You may use it at: ImageToBase64 or directly from here:


See this project on GitHub

06 January, 2013

Merge log files from multiple servers

If you have some similar servers (e.g. balanced EC2-instances) and need to analyze log files from them - it is possible to automatize downloading and merging all these files via shell script like this:

#!/bin/bash

LOG_DIR=log-`date +%Y.%m.%d_%H.%M.%S`

mkdir -p $LOG_DIR

scp -C ec2-user@node1.server.com:/log/file.log $LOG_DIR/file1.log
scp -C ec2-user@node2.server.com:/log/file.log $LOG_DIR/file2.log
scp -C ec2-user@node3.server.com:/log/file.log $LOG_DIR/file3.log

cat $LOG_DIR/*.log | sort >$LOG_DIR/files.log


Also it is a good idea to use ssh-agent to keep private keys.

05 January, 2013

Count string characters in Python

Simple code that calculates frequency of letters:
from collections import Counter

s = '''
She sells sea-shells on the sea-shore.
The shells she sells are sea-shells, I'm sure.
For if she sells sea-shells on the sea-shore
Then I'm sure she sells sea-shore shells.
'''

print Counter(s)


Result:
Counter({'s': 32, 'e': 29, ' ': 25, 'l': 18, 'h': 16, 'a': 7, 'r': 7, '-': 6, 'o': 6, '\n': 5, '.': 3, 'n': 3, "'": 2, 'I': 2, 'T': 2, ' m': 2, 'u': 2, 't': 2, ',': 1, 'F': 1, 'S': 1, 'f': 1, 'i': 1})

04 January, 2013

Recover broken Amazon EC2 instance

Sometimes shit happens, accidentally.
Especially  when you lost ssh access to your virtual server (e.g. due to errors in  ~/.ssh/authorized_keys, /etc/sudoers, /etc/group, /etc/shadow or another important config file).

When you catch errors like:

No supported authentication methods available

or

sudo
sudo: >>> /etc/sudoers: syntax error near line 1 <<<
sudo: parse error in /etc/sudoers near line 1
sudo: no valid sudoers sources found, quitting
sudo: unable to initialize policy plugin


Don't worry, inside the Amazon cloud you can repair almost all.

Follow these simple steps to solve the problem:
  1. Via AWS console:
    1. Launch new temporary EC2 micro-instance in the same availability zone.
    2. Stop broken instance.
    3. Detach root EBS volume from the broken instance.
    4. Attach this EBS to the temporary instance (as /dev/sdf).
  2. Via SSH console from the temporary instance:
    1. Mount this EBS:
      sudo mount /dev/xvdf /mnt
    2. Fix all problems on mounted file system.
    3. Unmount this EBS:
      sudo umount /mnt
  3. Via AWS console:
    1. Detach this EBS from the temporary instance.
    2. Attach this EBS to the broken instance (as /dev/sda1).
    3. Start broken instance.
  4. Check that broken instance now is healthy (available via ssh and everything is functioning normally).
  5. Then you can stop temporary instance (or terminate it)
I hope this article will make someone little bit happy)

03 January, 2013

Xameleon

Just released web-camflash application Xameleon
Have Fun at Xameleon :-)