Każde rozwiązanie potrzebuje problemu
Aby pokazać, że problem nie jest jakoś oderwany od rzeczywistości zerknijmy na tego oto linka : TomTomowe api do geocodowania adresów. A w zasadzie to zerknijmy tam na sam dół do sekcji "Batch queries". To jest opcja serwisu, która przyjmuje na chwilę obecną zapytania zawierające do 100 adresów i zwraca nam różne ciekawe informacje.
Tak dla ćwiczeń spróbujmy zaimplementować podobny serwis, który po prostu zwróci listę miast przesłanych w zapytaniach zawartych w pliku xml. I żeby było optymalnie to plik będzie spakowany zipem.
Co jest tak naprawdę "Logiką Biznesową"
Jest nią ta krótka funkcja :
def findCities(xml:Elem)=xml \\ "locations" \\ "location" \\ "L" map(_.text) reduce((a,b)=>s"$a,$b")I teraz kilka uwag co do tego kodu :
- Tak ta jedna linijka parsuje xml, wydobywa z niego wszystkie miasta i łączy je w jeden text oddzielając przecinkami.
- Tak, po pewnym czasie obcowania z językiem Scala do tej linijki można dołączyć przymiotnik czytelna
- I Nie, to nie jest część terapii leczenia kompleksów programistycznych poprzez obcowanie z jednolinijkowcami.
Problem jest taki, że zanim będziemy w stanie wykonać tę linijkę to musimy jakoś przyjąć zzipowany plik, rozpakować go, zrobić z niego typ XML i dopiero przekazać do logiki. Tutaj do akcji wkraczają parsery...
Parsery
Ponieważ krążą plotki, że pisanie parsera w playu od zera może wysłać programistę Javy na terapię wiec pokażemy jak łatwo komponować gotowe już parsery. Na początek mamy gotowca, który zapisuje przesłany plik tam gdzie chcemy. Tutaj na szybko przykład na pałę skopiowany z dokumentacji :
def save = Action(parse.file(to = new File("/tmp/upload"))){request=> Ok("Saved the request content to " + request.body) }Wygląda łatwo i używa się tego też bardzo łatwo. Teraz chcemy go użyć do stworzenia parsera, który nam jeszcze ten plik rozpakuje :
val uncompressBodyToTemp = parse.using { request => parse.file(createTempFile) map(unzip) }I to w zasadzie tyle. No prawie bo createTempFile i unzip trzeba napisać samemu ale nie jest to nic trudnego.
Interakcja z Javą
A Scala z Javą współpracuje bardzo dobrze. Poniżej np. widzimy wygodne wykorzystanie klasy Files z Javy 7.
def createTempFile = Files.createTempFile("geo_", "_file.zip").toFile()A teraz unzip. Tutaj taka ciekawostka - jak się pogugluje "scala unzip file" to się ch.. znajdzie. Za to do Javy bibliotek trochę jest. I bardzo łatwo te biblioteki w Scali zastosować. Tutaj użyjemy zip4j, która jest po prostu zajebista.
Na chwilę obecną jestem na etapie gdzie jeszcze nie rozumiem czym dokładnie SBT różni się od mavene (SBT to system budowania dla Scali) ale generalnie zależności z repo mavenowego dodaje się tam dosyć prosto
libraryDependencies ++= Seq( jdbc, anorm, cache, "net.lingala.zip4j" % "zip4j" % "1.3.2" <---- o tutaj to wkleiłem )Dobra to teraz funkcja "unzip" i kilka ciekawostek.
def unzip=(inputfile: File) => { import collection.JavaConverters._ //1 val zipFile=new ZipFile(inputfile) //2 val fileName=zipFile.getFileHeaders().asScala.head.asInstanceOf[FileHeader].getFileName //3 zipFile.extractFile(fileName,"/tmp") //4 new File(s"/tmp/$fileName") }
- Tutaj importujemy konwertery ubogich kolekcji w Javie do bogatych kolekcji w scali.
- Ta linia w Javie zmusiła by nas do obsługi checked exception
- Tutaj mamy przykład konwersji listy Javowej do Scalowej. Ponieważ ta lista sama w sobie jest zjebana gdyż nie ma generyków i przechowuje czyste obiekty toteż trzeba dać rzutowanie.
w javie ta linia wyglądałaby dla porównania następująco : (czytelność do oceny czytelnika):
- String fileName = ((FileHeader)zipFile.getFileHeaders().get(0)).getFileName();
To jak wygląda w końcu ta cała akcja
Ano wygląda tak :
def apiUpload=Action(uncompressBodyToTemp){request=> val xmlFile=scala.xml.XML.loadFile(request.body) Ok(findCities(xmlFile)) }Czy można jeszcze lepiej. Ano można - po cholerę akcja ma się zajmować pakowaniem pliku do xmla.
def convertToXml(file:java.io.File)=scala.xml.XML.loadFile(file) val uncompressBodyToXml= uncompressBodyToTemp map convertToXml def apiUpload=Action(uncompressBodyToXml){request=> Ok(findCities(request.body)) }Może miałem w życiu pecha ale nie widziałem żeby w czystej obiektówce można było uzyskać taki poziom kompozycji.
Hola hola a co z formularzami?
Poprzedni przykład był dobry dla usług, które oczekują, że plik będzie jedyną rzeczą jaka do nich przyjdzie w rekłeście. W przypadku formularzy nie można tego tak łatwo zrobić bo tam są multiparty i inne takie tam. Ale wiadmo - w playu jest gotowiec i na to : parse.multipartFormData
Kod może być na początku leciutko bardziej skomplikowany ale zaraz to sobie wytłumaczymy :
val uncompressFormToTemp = parse.using { request => val tmpFile = createTempFile parse.multipartFormData.map(multipartFormData=>multipartFormData.file("query") .map { filePart => filePart.ref moveTo (tmpFile, true) tmpFile }.map(unzip) ) }To byłą wersja wydłużona - a teraz wersja skrócona :
val uncompressFormToTemp = parse.using { request => val tmpFile = createTempFile parse.multipartFormData.map(_.file("query") .map { filePart => filePart.ref moveTo (tmpFile, true) tmpFile } map unzip ) }No może nie jest za bardzo skrócona ale to był tylko pretekst do małej dygresji o tych podkreślnikach co się w Scali tu i tam pojawiają :
Od czasu do czasu, ktoś obcujący ze Scalą może natrafić na "Emotikon Driven Development" np.
List(1,2,3) reduce(_+_) //sumuje wszystkie elementy listy List(1,2,3) reduce(_*_) //iloczyn wszystkich elementów listyMożna to także zapisać w sposób :
List(1,2,3) reduce((a,b)=>a+b) List(1,2,3) reduce((a,b)=>a*b)I teraz co następuje - gdy tak już masę razy - raz za razem zapisuje się kompletne deklaracje funkcji to pomału staje się to męczące. W pewnym momencie w głowie pojawia się trudna do wyartykułowania potrzeba uproszczenia tego zapisu. I nagle wokół literek na ekranie zaczyna się ujawniać niezwykła lekko zielonkawa świetlista otoczka - zaczynają się one niejako ruszać próbując ci coś przekazać. Ekran staje się giętki i plastyczny. Nie jesteś w stanie stwierdzić gdzie się kończy a gdzie zaczyna.
Nagle następuje ocknięcie. Rozglądasz się po pokoju i zauważasz, że wszystko wygląda tak samo a jednak coś się zmieniło. Twoja postać uzyskuje nową zdolność - czytanie kodu z podkreślnikami
Tego nie da się logicznie argumentować - to trzeba przeżyć
Akcja formularza
Niby tak samo ale trochę inaczej. Generalnie parser zamiast pliku zwraca nam Option[File] ponieważ coś tam w międzyczasie mogło pójść nie tak (ot nie dostaliśmy żadnego pliku w multiparcie)
def lift[A,B](f:A=>B):Option[A]=>Option[B]=_ map f val uncompressFormToXml =uncompressFormToTemp map lift(convertToXml)I tutaj z okazji, że będziemy operować na Option trzeba leciutko udekorować naszą funkcję konwersji do xml. Widzimy, że znowu bardzo ładnie się wszystko komponuje. Oczywiście funkcję lift można by nazwać ejWeNotMiToNAOpszionaPrzmien ale używając nomenklatury z fachowej literatury będę wyglądać na mądrzejszego niż w rzeczywistości jestem.
I w końcu akcja:
def upload = Action(uncompressFormToXml) { request => request.body.map{file=> Ok(findCities(file)) }.getOrElse(BadRequest) }I proszę "ło matko" obsługujemy zły request beż żadnych ifów. Jak coś się wyjebało to będzie BadRequest. i tyle. Sądzę, że cała wymieniona do tej pory logika zmieści się w kilkudziesięciu linijkach co odpowiada długości relatywnie dobrze utrzymanej metody Javowej. A czytelność - jak się nauczy Scali to się będzie umiało tym bawić.
Re użycie
Mamy nasze parserki i mogą one zacząć na siebie pracować. Stworzymy teraz funkcjonalność pro-edukacyjną, która ma obronić młodzież przed zbereźnymi tekstami. Np takimi jak w Panu Tadeuszu :
Iż ten urząd na zamku przed laty piastował.
I dotąd nosił wielki pęk kluczów za pasem,
Uwiązany na taśmie ze srebrnym kutasem
def cenzuruj(text:String)=text.replaceAll("kutas", "k___s")akcja:
def cenzura=Action(uncompressFormToTemp){request=> import scala.io.Source._ request.body.map{file=> val lines=fromFile(file).getLines() Ok(lines.map(cenzuruj).mkString("\n")) } getOrElse(BadRequest) }I młodzież uratowana.
* * *
Paweł, chyba spać nie możesz w niedzielę rano ;-)
OdpowiedzUsuń(Art +1)