alfresco the clojure way -- slides from the alfresco devcon2011

46
ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro The Clojure way Alfresco Carlo Sciolla, Sr R&D Developer at Backbase Thursday, November 10, 2011

Upload: carlo-sciolla

Post on 12-May-2015

1.621 views

Category:

Technology


1 download

DESCRIPTION

These slides were used for my Alfresco the Clojure way presentation talk at the Alfresco DevCon 2011 in London on November 10

TRANSCRIPT

Page 1: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

The Clojure wayAlfresco

Carlo Sciolla, Sr R&D Developer at Backbase

Thursday, November 10, 2011

Page 2: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

I would like, if I may, to take you on a strange journey.

Thursday, November 10, 2011

Page 3: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuroALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Sample user storyAs admin, I want to loop through all the nodes from the Alfresco repository, So that I can print their names

public void printAllNames(repo) { AuthenticationUtil.runAs(new RunAsWork () { public Object doWork(){ for (NodeRef node : getAllNodes(repo)) { String name = getName(node); System.out.println(name); } } }, AuthenticationUtil.getAdminUserNAme())}

Thursday, November 10, 2011

Page 4: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Sample user storyAs admin, I want to loop through all the nodes from the Alfresco repository, So that I can print their namesALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defn print-all-names [repo] (run-as (admin) (doseq [node (to-seq repo)] (println (property node “cm:name”)))))

#LOC < 1/2

Thursday, November 10, 2011

Page 5: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuroALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defn print-all-names [repo] (AuthenticationUtil/runAs (reify AuthenticationUtil$RunAsWork (doWork [this] (doseq [node (to-seq repo)] (println (property node “cm:name”))))) (admin)))

How to get thereHaving only moved parenthesis around and tweaked it a bit, we eventually translated our code into Clojure. We still have to get rid of the anonymous class.

Java interop

Thursday, November 10, 2011

Page 6: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuroALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defmacro run-as [user f] `(let [work# (reify AuthenticationUtil$RunAsWork (~'doWork [~'this] ~f))] (AuthenticationUtil/runAs work# ~user)))

Reflection on massive steroidsMacros are “special” functions that, at compile time, shuffle the pieces they receive in input and return a function. They’re misterious and powerful: don’t try to decode it now, young jedi.

Thursday, November 10, 2011

Page 7: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Straight to the pointClojure, as any Lisp, allows you to easily create clear and concise DSLs. New functions to enhance expressivity to your programs. Fewer lines to maintain.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defn print-all-names [repo] (run-as (admin) (doseq [node (to-seq repo)] (println (property node “cm:name”)))))

Thursday, November 10, 2011

Page 8: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Here be LambdalfWhile Clojure allows direct access to Java classes and instances, lambdalf provides a more idiomatic access to the core Alfresco API. You’ll see some examples along the way.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defn print-all-names [repo] (run-as (admin) (doseq [node (to-seq repo)] (println (property node “cm:name”)))))

https://github.com/skuro/lambdalf

Thursday, November 10, 2011

Page 9: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuroALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defn print-all-names [repo] (run-as (admin) (doseq [node (to-seq repo)] (println (property node “cm:name”)))))

Lost In Superfluous Parenthesis?It is usually perceived that Lisp code has an overwhelming amount of parenthesis.

Thursday, November 10, 2011

Page 10: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Lost In Superfluous Parenthesis?It is usually perceived that Lisp code has an overwhelming amount of parenthesis. Itʼs usually unfair.

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

VS

Parenthesis count

2618 24 w/expanded macro

Thursday, November 10, 2011

Page 11: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

the sexp and the cityor: How I learned to stop worrying and love the parens

Thursday, November 10, 2011

Page 12: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Symbolic ExpressionsList based data structures. They may be a nested list of smaller S-expressions. Best known for their use in Lisp.

(Wikipedia)ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

sexp = | primitive-elem | list of s-expression

Thursday, November 10, 2011

Page 13: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Primitive data typesBasic building blocks. Most of them should look familiar, or meaningful. Hopefully.

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Symbols: ns/foo odd? + this!is_a+single*symbol-42

Literals: “string” \c \space \tab 42 3.14 42/11 36r16 true false #”^[a-zA-Z]*” nil

Keywords: :key ::qualified :ns/qualified :can+be!weird

Thursday, November 10, 2011

Page 14: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Collections or sequencesAll aggregate data types can be encapsulated under the same interface: ISeq. Use the powerful Clojure sequence processing library to rule them all, but beware: they are all immutable!ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(\c “list” :something)

[“vector” 42 :foobar]

{:question “life, universe and everything” :answer 42}

#{“foo” 42 :bar}

list

vector

map

set

Thursday, November 10, 2011

Page 15: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

LISt ProcessingLists are king among data structures in any Lisp: they also happen to be code (homoiconicity). Lists are executed by evaluating the first symbol to a function, then calling it with the others as parameters.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(quote (\c “list” :something))list

(defn print-all-names [repo] (run-as (admin) (doseq [node (to-seq repo)] (println (property node “cm:name”)))))

function calls

‘(\c “list” :something)list

Thursday, November 10, 2011

Page 16: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

immutabilityand the art of motorcycle maintenance

Thursday, November 10, 2011

Page 17: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Where is the horse?The same “thing” such as a variable or an Object, can be completely different at different moments in time. We call such things identities, and they relate to a sequence of values.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

t

??

Horse horse = new Horse(14);horse.getPosition();

Thursday, November 10, 2011

Page 18: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Persistent data structuresAll data structures are immutable after their creation. As such, you never change them, but rather create new versions of it. We call them values, and are effectively easier to handle than Objects.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defn move [h] {:pos (inc (:pos h))})

(def horse {:pos 14})

(:pos horse) ; 14

(move horse) ; {:pos 15}

Thursday, November 10, 2011

Page 19: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Structural sharingWhen creating a new version of a value, Clojure runtime optimizes the process and allows different version of the same data structure to share parts in memory.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Thursday, November 10, 2011

Page 20: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

concurrency

Thursday, November 10, 2011

Page 21: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Shared mutable stateIt’s sometimes desirable to access the same identity from different threads. When going concurrent, several tasks that used to be trivial are now extremely hard. OO makes it even harder.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

private HashMap _map = new HashMap();

public void oneMore(Object k, Object v) { _map.put(k, v);}

#fail

Thursday, November 10, 2011

Page 22: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Software Transactional MemorySimilarly to databases, Clojure runtime uses transactions to handle concurrent access to shared mutable state. Every thread gets the current value, if changes occur outside, the transaction rolls back.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Thursday, November 10, 2011

Page 23: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Concurrency IS rocket scienceSorting out concurrency is an extremely difficult task. Clojure provides language level barriers against poorly designed concurrent access to shared mutable state.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(def _shared (ref {}))

(defn update [old k v] (assoc old k v))

(defn one-more [k v] (dosync (alter _shared update k v)))

Thursday, November 10, 2011

Page 24: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Thereʼs no silver bulletDifferent concurrent access patterns require different constructs. Programmers still have to pay attention to which kind of concurrency control to use.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

clojure.core/ref([x] [x & options])

clojure.core/atom([x] [x & options])

clojure.core/agent([state & options])

sync coord

Thursday, November 10, 2011

Page 25: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

the seq library

Thursday, November 10, 2011

Page 26: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

seq to rule them allQuite intentionally, you can reduce all Clojure data structures and Java collections, iterables, Strings and arrays to the very same interface.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

clojure.core/seq([coll]) Returns a seq on the collection. If the collection is empty, returns nil. (seq nil) returns nil. seq also works on Strings, native Java arrays (of reference types) and any objects that implement Iterable.

Thursday, November 10, 2011

Page 27: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

One-stop shopDealing with a single abstraction allows for the same functions to be applicable to an incredible number of problems. The richness of the seq library is the only swiss army knife you need.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

distinct filter remove for keep keep-indexed cons concat lazy-cat mapcat cycle interleave interposerest next fnext nnext drop drop-while nthnext for take take-nth take-while butlast drop-last forflatten reverse sort sort-by shuffle split-at split-with partition partition-all partition-bymap pmap mapcat for replace reductions map-indexed seque first ffirst nfirst second nth when-first last rand-nth zipmap into reduce set vec into-array to-array-2d frequencies group-by applynot-empty some reduce seq? every? not-every? not-any? empty? some filter doseq dorun doall seq vals keys rseq subseq rsubseq lazy-seq repeatedly iterate repeat replicate range line-seq resultset-seq re-seq tree-seq file-seq xml-seq iterator-seq enumeration-seq

Thursday, November 10, 2011

Page 28: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

fully functional

Thursday, November 10, 2011

Page 29: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Sequencing AlfrescoNodes in Alfresco are stored as a tree. Surely enough, trees can be represented as nested sequences, allowing for the seq library to disclose its power.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defn print-all-names [repo] (run-as (admin) (doseq [node (to-seq repo)] (println (property node “cm:name”)))))

Thursday, November 10, 2011

Page 30: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Depth first traversalUsing the standard seq library, you can compose a linear sequence out of a tree structure (of nested sequences).

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

distinct filter remove for keep keep-indexed cons concat lazy-cat mapcat cycle interleave interposerest next fnext nnext drop drop-while nthnext for take take-nth take-while butlast drop-last forflatten reverse sort sort-by shuffle split-at split-with partition partition-all partition-bymap pmap mapcat for replace reductions map-indexed seque first ffirst nfirst second nth when-first last rand-nth zipmap into reduce set vec into-array to-array-2d frequencies group-by applynot-empty some reduce seq? every? not-every? not-any? empty? some filter doseq dorun doall seq vals keys rseq subseq rsubseq lazy-seq repeatedly iterate repeat replicate range line-seq resultset-seq re-seq tree-seq file-seq xml-seq iterator-seq enumeration-seq

Thursday, November 10, 2011

Page 31: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Anatomy of a tree-seqFunctions are first class, and tree-seq is a higher order fn that takes two functions and a data structure to create a flat sequence.

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

clojure.core/tree-seq([branch? children root]) Returns a lazy sequence of the nodes in a tree, via a depth-first walk. branch? must be a fn of one arg that returns true if passed a node that can have children (but may not). children must be a fn of one arg that returns a sequence of the children. Will only be called on nodes for which branch? returns true. root is the root node of the tree.

Thursday, November 10, 2011

Page 32: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Thinking AlfrescoPeer to peer associations allow you to create logical trees regardless of files and folders structure. For the time being, let’s assume we’re only interested into parent child assocs between nodes.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

clojure.core/tree-seq([branch? children root]) Returns a lazy sequence of the nodes in a tree, via a depth-first walk. branch? must be a fn of one arg that returns true if passed a node that can have children (but may not). children must be a fn of one arg that returns a sequence of the children. Will only be called on nodes for which branch? returns true. root is the root node of the tree.

Thursday, November 10, 2011

Page 33: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

First stepWe know that cm:contains is provided as part of cm:folder definition. We check that the given node is of a compatible type. Lambdalf provides here some Clojure/Java type conversion.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defn branch? [node] (qname-isa? (type-qname node) (qname "cm:folder")))

(defn qname-isa? [child parent] (.isSubClass (dictionary-service) (qname child) (qname parent)))

(defn qname [qname-str] (let [[prefix name] (QName/splitPrefixedQName qname-str)] (QName/createQName prefix name (namespace-service))))

(defn type-qname [node] (m/qname (.getType (node-service) (c/c2j node))))

Thursday, November 10, 2011

Page 34: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Populate the seqNavigating the tree means getting the children of the traversed node, if any. This is food for the NodeService.

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

clojure.core/tree-seq([branch? children root]) Returns a lazy sequence of the nodes in a tree, via a depth-first walk. branch? must be a fn of one arg that returns true if passed a node that can have children (but may not). children must be a fn of one arg that returns a sequence of the children. Will only be called on nodes for which branch? returns true. root is the root node of the tree.

Thursday, November 10, 2011

Page 35: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

API bridgeLambdalf tries to shave the yak for you, and despite its code doesn’t shine by beauty, it allows for a clean user code. Again, Java and Clojure interact seamlessly here.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defn children [node] (n/children node)))

(defn children [node] (into #{} (doall (map #(c/j2c (.getChildRef %)) (.getChildAssocs (node-service) (c/c2j node))))))

Thursday, November 10, 2011

Page 36: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Wrapping it upMost of the mystery is now gone, the core of print-all-names has been demystified. While run-as is still magic, it’s easy to tell what’s for.

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defn print-all-names [repo] (run-as (admin) (doseq [node (to-seq repo)] (println (property node “cm:name”)))))

(defn to-seq [node] (tree-seq branch? children node))

Thursday, November 10, 2011

Page 37: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

the lord of the replone repl to bring them all and in the lexical scope, bind them

Thursday, November 10, 2011

Page 38: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Live codingSimilarly to scripted languages, your code is parsed and compiled into bytecode as soon as you enter it. In facts, you are altering the current runtime state.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

R

E

P

L

read

eval

print

loop

user> (defn Y [r] ((fn [f] (f f)) (fn [f] (r (fn [x] ((f f) x))))))#'user/Yuser>

prompt

result

Thursday, November 10, 2011

Page 39: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Swank serverOpening a REPL server is just one web script call away. You’ll need a client to connect, major IDE and editors have one. As usual, Emacs is superior :-P ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

$ curl -X POST -u admin:admin \ http://localhost:8080/alfresco/service/swank{port : 4005}

+

Thursday, November 10, 2011

Page 40: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

the clojure way

Thursday, November 10, 2011

Page 41: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Warning: alpha codeLambdalf and Clojure webscripts have a long way ahead in terms of API maturity and usability, but the basic pieces are out there for you to start your journey into this new, fancy Lisp.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defn query "Returns all the results of a search" ([q] (query StoreRef/STORE_REF_WORKSPACE_SPACESSTORE SearchService/LANGUAGE_LUCENE q)) ([store lang q] (with-open [rs (.query (search-service) store lang q)] (doall (map c/j2c (.getNodeRefs rs))))))

(map #(property % name) (query “@cm\:name:\”Sites\””))

Thursday, November 10, 2011

Page 42: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Warning: alpha codeLambdalf and Clojure webscripts have a long way ahead in terms of API maturity and usability, but the basic pieces are out there for you to start your journey into this new, fancy Lisp.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(write! [^String src node] (let [noderef (c/c2j node) w (.getWriter (content-service) noderef ContentModel/PROP_CONTENT true)] (.putContent w (ByteArrayInputStream. (.getBytes src "UTF-8")))))

(write! “new content” node)

Thursday, November 10, 2011

Page 43: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Warning: alpha codeLambdalf and Clojure webscripts have a long way ahead in terms of API maturity and usability, but the basic pieces are out there for you to start your journey into this new, fancy Lisp called Clojure.ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

(defn to-seq [root] (let [user (a/whoami) branch? (fn [x] (a/run-as user (m/qname-isa? (n/type-qname x) (m/qname "cm:folder")))) children (fn [x] (a/run-as user (n/children x)))] (tree-seq branch? children root)))

(to-seq (company-home))

Thursday, November 10, 2011

Page 44: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuroALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

John McCarthy4 September 1927 - 24 October 2011

Thursday, November 10, 2011

Page 45: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

Q / A

Thursday, November 10, 2011

Page 46: Alfresco the clojure way -- Slides from the Alfresco DevCon2011

ALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuroALFRESCO THE CLOJURE WAY | November 8, 2011 | @skuro

About me Content specialist, Clojurian, biker.

Carlo Sciolla

http://skuro.tk@skuro

sr. R&D Developer

Thank you! “A language that doesn't affect the way you think about programming, is not worth knowing”Alan J. Perlis

http://backbase.com

Thursday, November 10, 2011