Scala.js

Scaladores - Abril de 2016

Onilton Maciel

oniltonmaciel (gmail)

@oniltonmaciel

onilton

Front-end

Back-end

Zoeira/Besteirol

WHY
WHAT
HOW

JavaScript Linguagem

Origem

  • Criada em 1995 por Brendan Eich
  • Construída em < 1 semana e meia
  • Derivada do C. Inspiração Scheme
  • JavaScript - Marketing

javascript> ["10", "10", "10", "10"].map(parseInt)
javascript> ["10", "10", "10", "10"].map(function (x) { return parseInt(x) })
[10, NaN, 2, 3] // WHAAAAAT!!??
[10, 10, 10, 10] // Finalmente


Math.max(1, true);     // 1
Math.max(0, true);     // 1
Math.max(1, false);    // 1
Math.max(-1, true);    // 1
Math.max(-1, false);   // 0




'true' == true  // returns false

A parte boa

Douglas Crockford

 

JavaScript Experiência

Como eu me sinto quando...

...  vou testar o JS que acabei de escrever

 

Ufa, rodou! Agora é só torcer...

 

... para que algo ruim não aconteça

 

 

Single Page Applications

JavaScript Plataforma

Desktop Apps

NW.js (node-webkit)

Mobile

Tessel 2

http://blog.bruchez.name/2016/04/scala-on-tessel-2.html

class Point {
    x: number;
    y: number;

    constructor(x: number, y: number) {
        this.x = x;
        this.y = y;
    }

    getDist() { 
        return Math.sqrt(this.x * this.x + 
        this.y * this.y); 
    }
}

var p = new Point(3,4);
var dist = p.getDst();
alert("Hypotenuse is: " + dist);
case class Point(x: Int, y: Int) {
  def dist() = math.sqrt(x * x + y * y)
}

val p = Point(3,4)
val dist = p.dist()
println(s"Hypotunese is $dist")

WHY
WHAT
HOW

O quê funciona?

  • Suporte completo a linguagem Scala

  • Interoperabilidade completa com JavaScript

  • Código tão rápido quanto escrito em JS

  • Tamanho JS final "aceitável"

  • Ciclo rápido de mudanças

Crash course

build.sbt

project/

  build.sbt

index.html

src/

  main/

    scala/

      folder/

        Code.scala

 

addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.5")

build.sbt

project/

  build.sbt

index.html

src/

  main/

    scala/

      folder/

        Code.scala

 

// Turn this project into a Scala.js 
// project by importing these settings
enablePlugins(ScalaJSPlugin)

name := "ExampleProject"

version := "0.1-SNAPSHOT"

scalaVersion := "2.11.7"

persistLauncher in Compile := true

persistLauncher in Test := false

libraryDependencies ++= Seq(
  "org.querki" %%% "jquery-facade" % "1.0-RC2",
  "org.scala-js" %%% "scalajs-dom" % "0.8.2"
)

build.sbt

project/

  build.sbt

index.html

src/

  main/

    scala/

      folder/

        Code.scala

 

...

<script 
  type="text/javascript" 
  src="./target/scala-2.11/jsontocaseclass-opt.js">
</script>
<script 
  type="text/javascript"
  src="./target/scala-2.11/jsontocaseclass-launcher.js">
</script>

...
object Main extends js.JSApp {
  def main() = {
    var x = 0
    while(x < 10) x += 3
    println(x)
    // 12
  }
}

src/main/scala/folder/Code.scala

sbt fastOptJS

ScalaJS.c.LMain$.prototype.main__V = (function() {
  var x = 0;
  while ((x < 10)) {
    x = ((x + 3) | 0)
  };
  ScalaJS.m.s_Predef$()
           .println__O__V(x)
  // 12
});

sbt ~fastOptJS

sbt fullOptJS

be.prototype.main = (function() {
  for (var x=0; 10>x;)
    a=((a+3) | 0);
  ee(); L(); 
}

WHY
WHAT
HOW

http://json2caseclass.cleverapps.io/

Fork

Então...

$(function(){
   $("#caseclassform textarea").change(function(e){
      $('#mycodeis').html(t.scala_code({code:$(e.target).val()}));
      sh_highlightDocument();
   });
   $("#test_button").click(function(){
      $('#pastejsonform textarea').val('[{"coordinates":null,"favorited":false,"truncated":false,"created_at":"Mon Sep 03 13:24:14 +0000 2012","id_str":"242613977966850048","entities":{"urls":[],"hashtags":[],"user_mentions":[{"name":"Jason Costa","id_str":"14927800","id":14927800,"indices":[0,11],"screen_name":"jasoncosta"},{"name":"Matt Harris","id_str":"777925","id":777925,"indices":[12,26],"screen_name":"themattharris"},{"name":"ThinkWall","id_str":"117426578","id":117426578,"indices":[109,119],"screen_name":"thinkwall"}]},"in_reply_to_user_id_str":"14927800","contributors":null,"text":"@jasoncosta @themattharris Hey! Going to be in Frisco in October. Was hoping to have a meeting to talk about @thinkwall if you\'re around?","retweet_count":0,"in_reply_to_status_id_str":null,"id":242613977966850048,"geo":null,"retweeted":false,"in_reply_to_user_id":14927800,"place":null,"user":{"profile_sidebar_fill_color":"EEEEEE","profile_sidebar_border_color":"000000","profile_background_tile":false,"name":"Andrew Spode Miller","profile_image_url":"http://a0.twimg.com/profile_images/1227466231/spode-balloon-medium_normal.jpg","created_at":"Mon Sep 22 13:12:01 +0000 2008","location":"London via Gravesend","follow_request_sent":false,"profile_link_color":"F31B52","is_translator":false,"id_str":"16402947","entities":{"url":{"urls":[{"expanded_url":null,"url":"http://www.linkedin.com/in/spode","indices":[0,32]}]},"description":{"urls":[]}},"default_profile":false,"contributors_enabled":false,"favourites_count":16,"url":"http://www.linkedin.com/in/spode","profile_image_url_https":"https://si0.twimg.com/profile_images/1227466231/spode-balloon-medium_normal.jpg","utc_offset":0,"id":16402947,"profile_use_background_image":false,"listed_count":129,"profile_text_color":"262626","lang":"en","followers_count":2013,"protected":false,"notifications":null,"profile_background_image_url_https":"https://si0.twimg.com/profile_background_images/16420220/twitter-background-final.png","profile_background_color":"FFFFFF","verified":false,"geo_enabled":true,"time_zone":"London","description":"Co-Founder/Dev (PHP/jQuery) @justFDI. Run @thinkbikes and @thinkwall for events. Ex tech journo, helps run @uktjpr. Passion for Linux and customises everything.","default_profile_image":false,"profile_background_image_url":"http://a0.twimg.com/profile_background_images/16420220/twitter-background-final.png","statuses_count":11550,"friends_count":770,"following":null,"show_all_inline_media":true,"screen_name":"spode"},"in_reply_to_screen_name":"jasoncosta","source":"<a href=\\"http://www.journotwit.com\\" rel=\\"nofollow\\">JournoTwit</a>","in_reply_to_status_id":null},{"coordinates":{"coordinates":[121.0132101,14.5191613],"type":"Point"},"favorited":false,"truncated":false,"created_at":"Mon Sep 03 08:08:02 +0000 2012","id_str":"242534402280783873","entities":{"urls":[],"hashtags":[{"text":"twitter","indices":[49,57]}],"user_mentions":[{"name":"Jason Costa","id_str":"14927800","id":14927800,"indices":[14,25],"screen_name":"jasoncosta"}]},"in_reply_to_user_id_str":null,"contributors":null,"text":"Got the shirt @jasoncosta thanks man! Loving the #twitter bird on the shirt :-)","retweet_count":0,"in_reply_to_status_id_str":null,"id":242534402280783873,"geo":{"coordinates":[14.5191613,121.0132101],"type":"Point"},"retweeted":false,"in_reply_to_user_id":null,"place":null,"user":{"profile_sidebar_fill_color":"EFEFEF","profile_sidebar_border_color":"EEEEEE","profile_background_tile":true,"name":"Mikey","profile_image_url":"http://a0.twimg.com/profile_images/1305509670/chatMikeTwitter_normal.png","created_at":"Fri Jun 20 15:57:08 +0000 2008","location":"Singapore","follow_request_sent":false,"profile_link_color":"009999","is_translator":false,"id_str":"15181205","entities":{"url":{"urls":[{"expanded_url":null,"url":"http://about.me/michaelangelo","indices":[0,29]}]},"description":{"urls":[]}},"default_profile":false,"contributors_enabled":false,"favourites_count":11,"url":"http://about.me/michaelangelo","profile_image_url_https":"https://si0.twimg.com/profile_images/1305509670/chatMikeTwitter_normal.png","utc_offset":28800,"id":15181205,"profile_use_background_image":true,"listed_count":61,"profile_text_color":"333333","lang":"en","followers_count":577,"protected":false,"notifications":null,"profile_background_image_url_https":"https://si0.twimg.com/images/themes/theme14/bg.gif","profile_background_color":"131516","verified":false,"geo_enabled":true,"time_zone":"Hong Kong","description":"Android Applications Developer,  Studying Martial Arts, Plays MTG, Food and movie junkie","default_profile_image":false,"profile_background_image_url":"http://a0.twimg.com/images/themes/theme14/bg.gif","statuses_count":11327,"friends_count":138,"following":null,"show_all_inline_media":true,"screen_name":"mikedroid"},"in_reply_to_screen_name":null,"source":"<a href=\\"http://twitter.com/download/android\\" rel=\\"nofollow\\">Twitter for Android</a>","in_reply_to_status_id":null}]');
      $('#pastejsonform').submit();
   });

   $("#test_button2").click(function(){
      $('#pastejsonform textarea').val('{"results":[{"address_components":[{"long_name":"1600","short_name":"1600","types":["street_number"]},{"long_name":"Amphitheatre Pkwy","short_name":"Amphitheatre Pkwy","types":["route"]},{"long_name":"Mountain View","short_name":"Mountain View","types":["locality","political"]},{"long_name":"Santa Clara","short_name":"Santa Clara","types":["administrative_area_level_2","political"]},{"long_name":"California","short_name":"CA","types":["administrative_area_level_1","political"]},{"long_name":"United States","short_name":"US","types":["country","political"]},{"long_name":"94043","short_name":"94043","types":["postal_code"]}],"formatted_address":"1600 Amphitheatre Pkwy, Mountain View, CA 94043, USA","geometry":{"location":{"lat":37.42291810,"lng":-122.08542120},"location_type":"ROOFTOP","viewport":{"northeast":{"lat":37.42426708029149,"lng":-122.0840722197085},"southwest":{"lat":37.42156911970850,"lng":-122.0867701802915}}},"types":["street_address"]}],"status":"OK"}');
      $('#pastejsonform').submit();
   });

});
$(function(){
   $('#pastejsonform').submit(function(e){
      e.preventDefault();
      $('#optionzone').html('<form class="form-horizontal" id="json_analisys_zone">'
         +'                 <h2>Json analysis</h2>'
         +'                 <div id="alertplace"></div>'
         +'                 <div id="classesplace"></div>'
         +'                 <button type="submit" class=" pull-right btn btn-primary"><i class="icon-cogs"></i> re-generate</button>'
         +'                 </form>');

      $('#json_analisys_zone').submit(re_generate_scala);
      var o = null;
      try{
         o = JSON.parse($(e.target).find('textarea').val());
      }catch(e){
         $('#alertplace').append(t.error({value:'The json root is invalid...'}));
         return 1;
      }

      if(_.isArray(o)){
         $('#alertplace').append(t.alert({value:'The json root is an array, only the first entity will be analyse...'}));
         o = o[0];
      }

      if(!_.isObject(o)){
         $('#alertplace').append(t.error({value:'The json root is not an object, cannot do anything for you...'}));
         return 1;
      }




      analyse_object(o, 'r00tJsonObject');


      $('#alertplace').append(t.info({value:$('#classesplace div.one_class').length+' case class generated'}));

      $('input.class_name').each(function(i,ii){
         maj_name({target:ii});
      });

      generate_scala($('#classesplace'));

      $('input.class_name').change(maj_name);

      $('#classesplace input').change(re_generate_scala);


   });

});

// HERE CAN BE SOME CONFIG PLACE

var scala_words =  ['abstract','case','catch','class','def','do','else','extends','false','final','finally','for','forSome','if','implicit','import','lazy','match','new','null','object','override','package','private','protected','return','sealed','super','this','throw','trait','try','true','type','val','var','while','with','yield'];
var scala_chars = ['-', '_'];
var scala_types = ['List', 'Type', 'Meta', 'Result'];

for(var i in scala_words){
  var oname = scala_words[i];
  scala_types.push(oname.charAt(0).toUpperCase() + oname.substring(1));
}

//

var analyse_object = function(o, oname){
   oname = generate_name(oname);
   var sign = generate_signature(o);
   if($('#class_'+sign).length > 0){
   //   console.log('class already analyse');
   }else{
   var elem = $(t.one_class({oname:oname, sha: sign}));
   var elem_u = elem.find('div.ul');
   if(_.size(o) > 22){
      $('#alertplace').append(t.error({value:'the '+ oname + ' class is exceding 22 fields, generated but it will not work, due to the Product arity limitation'}));
   }



   _.each(o, function(value, key, list){
      var ts = "String";
      var sha = "";
      var list = "";
      var disabled = "";


      if(_.isString(value)){
         ts = "String";
      }
      if(_.isNumber(value)){
         ts = "Double";
      }
      if(_.isBoolean(value)){
         ts = "Boolean";
      }
      if(_.isDate(value)){
         ts = "Date";
      }

      if(_.isArray(value)){
         if(is_value_consistent(value)){
            list='List';
            disabled = "disabled";
            ts = generate_name(list + '['+generate_name(key)+']');
            if(_.size(value) == 0){
               $('#alertplace').append(t.error({value:'the '+ oname +' '+key+ ' field is an empty array : cannot analyse :-('}));
            }else{
               if(_.isObject(value[0])){
                  sha=generate_signature_collection(value);
                  analyse_object(value[0], key);
               }else{
                  var vv = value[0];
                  var ts2 = "String"
                  disabled = "";
                  if(_.isString(vv)){
                     ts2 = "String";
                  }
                  if(_.isNumber(vv)){
                     ts2 = "Double";
                  }
                  if(_.isBoolean(vv)){
                     ts2 = "Boolean";
                  }
                  if(_.isDate(vv)){
                     ts2 = "Date";
                  }
                  ts = generate_name(list + '['+ts2+']');
               }
            }
         }else{
            $('#alertplace').append(t.error({value:'the '+ oname +' '+key+ ' field is prentending an array but not consistent'}));
         }
      }

      if(_.isObject(value) && !_.isArray(value)){
         ts = generate_name(key);
         disabled = "disabled";
         sha = generate_signature(value);
         analyse_object(value, key);
      //   }
      }



      elem_u.append(t.one_line({name:key,typescala:ts,sha:sha,disabled:disabled,list:list, oname:oname}));
   }, this);
   elem.append(t.info({value:elem_u.find('.li').length+' fields'}));

   $('#classesplace').append(elem);
   }

};

var sanitize_var_name = function(name){
   if(name.match(/[_a-zA-Z0-9]+/) == name && !_.contains(scala_words, name)){
      return name;
   }else{
      return '`' + name + '`';
   }
}

var generate_scala = function(el){
   var content = "";
   _.each(el.find('.one_class'), function(value, key, list){
      value = $(value);
      var props = _.map(value.find('.li'), function(v, k, l){
         var v = $(v);
         var sst = v.find('input.typescala').val();
          if ( v.find('input.optional_value[type="checkbox"]').prop("checked") ){
             sst = 'Option['+sst+']';
          }
         return '  ' + sanitize_var_name(v.find('label.keyname').text()) + ': ' + sst;
      }, this);
      content += t.one_scala_cclass({cname:value.find('input.class_name').val(), ccontent: props.join(',\n')}) + '\n';
   }, this);
   $('#caseclassform textarea').val(content);
   $('#mycodeis').html(t.scala_code({code:content}));
   sh_highlightDocument();
};

var maj_name = function(e){
   var elem = $(e.target);
   var tochange = $('div.ul input[data-signature-class="'+elem.attr('data-signature-class')+'"]');
   tochange.filter('input[data-list=""]').val(elem.val());
   tochange.filter('input[data-list="List"]').each(function(i){
      var ee = $(this);
      ee.val(ee.attr('data-list')+'['+elem.val()+']');
   });
   tochange.filter('input[data-list="Map"]').each(function(i){
      var ee = $(this);
      ee.val(ee.attr('data-list')+'[Map,'+elem.val()+']');
   });
};

var re_generate_scala =function(e){
      e.preventDefault();
      generate_scala($('#classesplace'));
};

var is_value_consistent = function(o){
   if(_.size(o) == 0){
      return true;
   }else{
   if(!_.isArray(o)){
      o = _.values(o);
   }
   var n = o[0];
   var nn = (_.isObject(n) ? generate_signature(n) : typeof n);
   return _.every(o, function(n){ return (_.isObject(n) ? generate_signature(n) : typeof n) == nn; }, this);
   }
};
var generate_signature_collection =function(o){
   if(_.size(o) == 0){
      return 0;
   }else{
   if(!_.isArray(o)){
      o = _.values(o);
   }
   return generate_signature(o[0]);
}
};
var generate_signature =function(o){
   if(_.isObject(o)){
      return SHA1(_.map(_.keys(o), function(n){ return n.toLowerCase(); }).sort().join('|'));
   }else{
      return SHA1(_.map(o, function(n){ return typeof n; }).sort().join('|'));
   }
};

var generate_name = function(oname){
  var n = (oname.charAt(0).toUpperCase() + oname.substring(1));
  if(_.contains(scala_types, n)){
    n += 'Bis';
  }
   return n;
};


var t = {
   alert :  _.template('<div class="alert">'
         +'<button type="button" class="close" data-dismiss="alert">×</button>'
         +'<i class="icon-warning-sign"></i> <%= value %>'
         +'</div>'),
   error :  _.template('<div class="alert alert-error">'
         +'<button type="button" class="close" data-dismiss="alert">×</button>'
         +'<i class="icon-warning-sign"></i> <%= value %>'
         +'</div>'),
   info :  _.template('<div class="alert alert-info">'
         +'<button type="button" class="close" data-dismiss="alert">×</button>'
         +'<%= value %>'
         +'</div>'),
   one_line :  _.template('<div class="li control-group">'
         +'<label class="keyname control-label"><%= name %></label> '
         +'<div class="controls">'
         +'<div class="input-append"><input class="typescala" <%= disabled %> type="text" data-signature-class="<%= sha %>" data-list="<%= list %>" value="<%= typescala %>" />'
         +' <span class="add-on"><input class="optional_value" type="checkbox" value="" id="chkb_<%= oname %>_<%= name %>" /><label class="label_chkbr" for="chkb_<%= oname %>_<%= name %>"> optional</label></span>'
         +'</div>'
         +'</div>'
         +'</div>'
         +''),
   one_class :  _.template('<div id="class_<%= sha %>" class="one_class">'
         +'<fieldset>'
         +'<div class="class_title"><i class="icon-leaf"></i> <input class="class_name" data-signature-class="<%= sha %>" type="text" value="<%= oname %>" /></div>'
         +'<div class="ul"></div>'
         +'</fieldset>'
         +'</div>'),
   one_scala_cclass : _.template('case class <%= cname %>(\n<%= ccontent %>\n)'),
   one_scala_props : _.template('<%= pname %>\:<%= ptype %>'),
   scala_code : _.template('<pre class="sh_scala"><%= code %></pre>')

};

sed 's/;$//g'

Tipos em todo lugar!

function -> def

var sanitize_var_name = function(name) {
   if(name.match(/[_a-zA-Z0-9]+/) == name && !_.contains(scala_words, name)){
      return name;
   }else{
      return '`' + name + '`';
   }
}
def sanitize_var_name(name: String): String = {
  /* java's String.matches all input data */
  if (name.matches("[_a-zA-Z0-9]+") && !scala_words.contains(name)) { 
    return name
  } else {
    return '`' + name + '`'
  }
}
var generate_scala = function(el){
   var content = "";
   _.each(el.find('.one_class'), function(value, key, list){
      value = $(value);
      var props = _.map(value.find('.li'), function(v, k, l){
         var v = $(v);
         var sst = v.find('input.typescala').val();
          if ( v.find('input.optional_value[type="checkbox"]').prop("checked") ){
             sst = 'Option['+sst+']';
          }
         return '  ' + sanitize_var_name(v.find('label.keyname').text()) + ': ' + sst;
      }, this);
      content += t.one_scala_cclass({cname:value.find('input.class_name').val(), ccontent: props.join(',\n')}) + '\n';
   }, this);
   $('#caseclassform textarea').val(content);
   $('#mycodeis').html(t.scala_code({code:content}));
   sh_highlightDocument();
};
def generate_scala(el: ???): Unit = {
   ....
}

x: js.Dynamic

var generate_scala = function(el){
   var content = "";
   _.each(el.find('.one_class'), function(value, key, list){
      value = $(value);
      var props = _.map(value.find('.li'), function(v, k, l){
         var v = $(v);
         var sst = v.find('input.typescala').val();
          if ( v.find('input.optional_value[type="checkbox"]').prop("checked") ){
             sst = 'Option['+sst+']';
          }
         return '  ' + sanitize_var_name(v.find('label.keyname').text()) + ': ' + sst;
      }, this);
      content += t.one_scala_cclass({cname:value.find('input.class_name').val(), ccontent: props.join(',\n')}) + '\n';
   }, this);
   $('#caseclassform textarea').val(content);
   $('#mycodeis').html(t.scala_code({code:content}));
   sh_highlightDocument();
};
def generate_scala(el: js.Dynamic): Unit = {
  var content = "";
  //....
}
calculaSalario(el.find('input.salario').text().asInstanceOf[Float])

No começo, abrace o asInstanceOf[T] 

Deixe os returns em paz

js.Dynamic.global

// Original JS
SHA1("testestring")

// Scala
js.Dynamic.global.SHA1("testestring")

// Shorter version
import js.Dynamic.{global => g}
g.SHA1("testestring")
// Underscore.js - Original JS
_.size(lista)

// Scala
g.`_`.size(lista) // js.Dynamic

val size = g.`_`.size(lista).asInstanceOf[Int]

Facade

@js.native
trait Underscore extends js.Object {
  def size(x: js.Dynamic): Int = js.native
}

@JSName("_")
object _u extends Underscore

val size = _u.size(lista)
var generate_scala = function(el){
   var content = "";
   _.each(el.find('.one_class'), function(value, key, list){
      value = $(value);
      var props = _.map(value.find('.li'), function(v, k, l){
         var v = $(v);
         var sst = v.find('input.typescala').val();
          if ( v.find('input.optional_value[type="checkbox"]').prop("checked") ){
             sst = 'Option['+sst+']';
          }
         return '  ' + sanitize_var_name(v.find('label.keyname').text()) + ': ' + sst;
      }, this);
      content += t.one_scala_cclass({cname:value.find('input.class_name').val(), ccontent: props.join(',\n')}) + '\n';
   }, this);
   $('#caseclassform textarea').val(content);
   $('#mycodeis').html(t.scala_code({code:content}));
   sh_highlightDocument();
};
  def generateScala(el: JQuery): Unit = {
    val content = el.find(".one_class").mapElems { value =>
      val jvalue = $(value)
      val props = jvalue.find(".li").mapElems { v =>
        val jv = $(v)
        val sst = jv.find("input.typescala").valueString

        val finalSst = if (jv.find("""input.optional_value[type="checkbox"]""").prop("checked").orNull.asInstanceOf[Boolean]) {
          s"Option[$sst]"
        } else sst

        "  " + sanitizeVarName(jv.find("label.keyname").text()) + ": " + finalSst
      }

      t.oneScalaCClass(jvalue.find("input.class_name").valueString, props.mkString(",\n"))
    }.mkString("\n")

    $("#caseclassform textarea").value(content)
    $("#mycodeis").html(t.scalaCode(content))

    g.sh_highlightDocument()
  }

Conclusões

  • Scala.js está pronto
  • No dia-a-dia, funciona como Scala na JVM, com pouquíssimas diferenças e limitações
  • A maior dificuldade é a integração com JS existente, tem uma pequena curva de aprendizado, mas muito relacionado ao DOM.
  • Há muitas facades prontas, mas construir sua própria não é tão difícil
  • Iniciantes, comecem com Scala

Referências e recursos

  • https://www.scala-js.org
  • https://github.com/sjrd/scala-js-example-app
  • http://www.lihaoyi.com/hands-on-scala-js/
  • http://ochrons.github.io/scalajs-spa-tutorial/
  • https://gitter.im/scala-js/scala-js

Scalajs - Scaladores Abril 2016

By Onilton Maciel

Scalajs - Scaladores Abril 2016

  • 708