Berichten tagged CouchDB
De _changes API van CouchDB in een Android MapView applicatie
26 sep
Soms kun je hele simpele zaken combineren, en daarmee toch hele interessante functionaliteit bereiken. Dit geldt ook voor de combinatie tussen een MapView en een _changes feed van CouchDB. Iedereen kent vast de Hello MapView tutorial wel. Hierin wordt uitgelegd hoe je op een eenvoudige manier eigen Points Of Interest (POIs) op een MapView als overlay kunt toevoegen. De enige vraag blijft nu: “Waar haal je nu die POIs vandaan?”. Nu kun je natuurlijk een of andere statische lijst ophalen en die over de MapView heen leggen, maar dan ben je natuurlijk snel uitgekeken. Mij leek het wat leuker om een iets dynamischere versie te maken, met POIs die actuele events weergeven. Dit had ik ook al eens gedaan voor de iPhone; nu wou ik dat voor elkaar krijgen in Android.
CouchDB is een noSQL oftewel document database. De documenten bestaan uit JSON objecten. Elk welgevormd JSON object kan in zo’n database opgeslagen worden. Een eenvoudige REST interface stelt ons in staat documenten toe te voegen, aan te passen en te verwijderen. Daarnaast is het ook mogelijk te ‘luisteren’ naar alle veranderingen die op de database plaatsvinden. Dit kan door middel van de _changes API. Een simpele HTTP GET naar de database (bijvoorbeeld http://localhost:5984/db/_changes) levert alle (meest recente) aanpassingen op alle documenten op. Wederom wordt dan een welgevormd JSON object geretourneerd. Het mooie aan deze _changes API is, dat je ook kunt aangeven vanaf welke verandering je de aanpassingen wilt volgen. Dit kan door middel van de request parameter ‘since’. In de ‘since’ parameter moet je dan de update sequence opgeven vanaf wanneer je de updates wilt ontvangen. Zo kun je bijvoorbeeld aangeven dat je alleen aanpassingen vanaf ‘nu’ wilt ontvangen. Als je niet weet wat de huidige update sequence is, kun je die opvragen door de database meta-data op te vragen (dit gaan via bijvoorbeeld http://localhost:5984/db). Dit levert bijvoorbeeld:
{"db_name":"db","doc_count":30,"doc_del_count":0,"update_seq":32,
"purge_seq":0,"compact_running":false,"disk_size":237657,
"instance_start_time":"1285418445080300","disk_format_version":5,
"committed_update_seq":32}
Waar de ‘update_seq’ staat voor de allerlaatste update. Op basis hiervan kun je dus alle vorige updates negeren door de ‘since’ parameter een waarde ’32′ mee te geven. Dit is mooie functionaliteit, maar het zou beter zijn als je een notificatie kunt krijgen van updates op het moment dat ze plaats vinden! Gelukkig is ook dat mogelijk.
De _changes API laat toe een request parameter ‘feed=continuous’ mee te geven. Als je dat doet (in combinatie met de ‘since’ parameter) zal de _changes feed vanaf dat moment voor alle aanpassingen op de database een welgevormd JSON object uitsturen. Dit gebeurt elke keer op een enkele regel, dus het inlezen is heel eenvoudig; Als voorbeeld:
http://localhost:5984/db/_changes?feed=continuous&include_docs=true&since=35"
{"seq":36,"id":"f0a4a559fac65ab3cca87baf1cf8f007",
"changes":[{"rev":"1-3b5146f14cd4a82d0a7075f0e59c8d9a"}],\
"doc":{\
"_id":"f0a4a559fac65ab3cca87baf1cf8f007",\
"_rev":"1-3b5146f14cd4a82d0a7075f0e59c8d9a",\
"type":"EVENT","name":"example1",\
"lat":42.036964,"lng":3.624541,"email":"user@example.com"}\
}
.
.
.
{"seq":37,"id":"f0a4a559fac65ab3cca87baf1cf905d1",\
"changes":[{"rev":"1-3b5146f14cd4a82d0a7075f0e59c8d9a"}],\
"doc":{"_id":"f0a4a559fac65ab3cca87baf1cf905d1",\
"_rev":"1-3b5146f14cd4a82d0a7075f0e59c8d9a",\
"type":"EVENT","name":"example2",\
"lat":42.036464,"lng":3.624541,"email":"user@example.com"}\
}
{"last_seq":37}
Waar de puntjes staan kunnen meerdere secondes verlopen zijn (de opmaak is wat aangepast voor de leesbaarheid). Als er langer dan 1 minuut geen updates zijn (server-side) dan zal de verbinding verbroken worden. Voordat dit gebeurt, laat de _changes feed nog even weten wat de laatste update_seq was (37 in dit geval). Dit maakt het wat gemakkelijker om opnieuw een verbinding met de _changes API te maken (als request parameter ‘since’ zal dan ’37′ gebruikt moeten worden). In bovenstaande URL is nog een request parameter opgenomen: ‘include_docs’. Dit zorgt ervoor dat niet alleen de document ids van de aangepaste (of toegevoegde) documenten in de feed terecht komen, maar ook de inhoud van de documenten! Dit maakt het mogelijk meteen de inhoud van de aangepaste documenten te gebruiken.
Na deze inleiding in de _changes feed kunnen we weer terug naar Android. In Android is het wel heel erg makkelijk om gebruik te maken van deze _changes feed: maak een URL aan, open de InputStream, en maak op basis van die InputStream een BufferedReader aan:
URL url = new URL("http://localhost:5984/db/_changes?feed=continuous&include_docs=true&since=35");
BufferedReader reader = new BufferedReader(new InputStreamReader(url.openStream(), "UTF-8"));
Nu is het slechts een kwestie van regels lezen, omzetten naar JSON en binnen je applicatie gebruiken.
Hiervoor heb ik (net als in het Hello MapView voorbeeld) een eigen ItemizedOverlay aangemaakt. Elke keer dat er een event binnenkomt via de _changes API wordt op basis van dit object een OverlayItem aangemaakt en aan de custom ItemizedOverlay toegevoegd:
private void addEvent(JSONObject doc) {
int late6 = (int)Math.round(doc.optDouble("lat", 0)*1E6);
int lnge6 = (int)Math.round(doc.optDouble("lng", 0)*1E6);
GeoPoint point = new GeoPoint(late6,lnge6);
String title = doc.optString("name", "unknown");
String email = doc.optString("email", "unknown");
OverlayItem overlayitem = new OverlayItem(point, title, email);
Drawable marker = null;
try {
marker = m_gravatar.getDrawable(email);
} catch (Exception e) {
e.printStackTrace();
}
m_events.addOverlay(overlayitem, marker);
m_mapView.postInvalidate();
}
waarin het m_gravatar object een custom class is die mij in staat stelt een plaatje bij gravatar.com op te halen, dat bij het email adres hoort.
Mijn custom ItemizedOverlay heeft in plaats van addOverlay(OverlayItem) een addOverlay(OverlayItem, Drawable) methode, om zo elk OverlayItem een eigen plaatje te kunnen geven.
public void addOverlay(OverlayItem item, Drawable balloon) {
if (balloon != null) {
item.setMarker(boundCenterBottom(balloon));
}
m_overlays.add(item);
populate();
//now mark it for deletion
m_handler.postDelayed(new ItemRemover(item), 8000);
}
Om te voorkomen dat de MapView helemaal vol met OverlayItems komt te staan, zorg ik ervoor dat elk OverlayItem na 8 seconden wordt verwijderd:
private class ItemRemover implements Runnable {
.
.
.
public void run() {
m_overlays.remove(m_item);
m_mapView.postInvalidate();
}
}
De postInvalidate() op de MapView zorgt ervoor dat de verwijderde overlays meteen verschijnen/ verdwijnen.
Zoals ik al in het begin vertelde: het combineren van enkele simpele technologieën kan toch al snel leuke functionaliteit opleveren. Dit simpele voorbeeld kan makkelijk uitgebouwd worden tot een applicatie die de gebruikers op een dynamische (real-time!) manier van locatie-gerelateerde informatie voorziet. Je zou bijvoorbeeld op eenvoudige wijze een applicatie kunnen bouwen die je eigen locatie naar de server stuurt. Iedereen die naar deze updates luistert zal automatisch jouw locatie op de kaart kunnen zien (en jij natuurlijk hun locatie).
CouchDB
26 sep
Enige tijd geleden ben in begonnen met CouchDB te spelen. Kennis maken met nieuwe technologieën gaat wat mij betreft altijd het beste door het bedenken van een soort project. Nu CouchDB 1.0.0 (1.0.1 is nu je beste optie!) is uitgebracht dacht ik dat het goed zou zijn om (eindelijk) een blog over mijn bevindingen tot nu toe te schrijven.
Ik bedacht het volgende project: Een eenvoudige (OSGi) log listener die zijn log berichten in een CouchDB database opslaat.
In Luminis maken we veel gebruik van OSGi. Het is een modulaire framework (geschreven in Java) die je toestaat om verschillende modules (de zogenaamde ‘bundels’) te combineren. Elke module kan zelfstandig werken of maakt gebruik van de functionaliteiten die door andere bundels worden aangeboden. Een slimme combinatie van verschillende bundels zal (bijna als ‘emerging behavior’) resulteren in een volledige applicatie die perfect bij uw behoeften past. De functionaliteit van een bepaalde bundel wordt beschikbaar gemaakt via interfaces, zodat implementaties kunnen veranderen zonder de interactie met andere bundels te veranderen.
Een van de compendium diensten die beschikbaar zijn is de LogService. Deze service biedt andere services de mogelijkheid berichten in een centrale log aan te maken. Het is vervolgens aan de logservice implementatie waar deze log berichten belanden. Logging wordt meestal verstuurd naar je console of een bestand, wat goed is in de meeste gevallen. Soms echter, is inloggen op het apparaat zelf en het ophalen van het logboekbestand voor inspectie geen gemakkelijke taak. Neem bijvoorbeeld een Android toestel. Met dank aan Aaron Miller’s werk, we zijn nu in staat om CouchDB draaien op Android. Android apps (EZDroid apps) zijn nu in staat om hun logging gegevens op te slaan in deze lokale instantie van CouchDB. Deze gegevens kunnen vervolgens eenvoudig worden gerepliceerd naar een andere instantie voor inspectie.
Ook voor beperkte apparaten (waar er bijvoorbeeld beperkte opslagruimte beschikbaar is) loggen naar een
bestand is gewoon geen optie.
Daarom dacht ik aan de volgende scenario’s waar
- CouchDB is lokaal geïnstalleerd op het apparaat/server. Toegang tot CouchDB is dan gegarandeerd en geen logging wordt gemist. Een nadeel zou kunnen zijn dat de OSGi applicatie een lokale installatie van CouchDB vereist (voor degenen die dat als een nadeel zien!).
- CouchDB is geïnstalleerd op een externe instantie. Toegang tot de CouchDB instantie zou wegens het netwerk instabiliteit kunnen worden onderbroken. Dan kunnen sommige log berichten verloren gaan. Omdat de meeste applicaties een werkende internet verbinding vereisen, ik denk dat we hiermee kunnen leven.
Het doel van het project is een JSON representatie van de werkelijk gelogde berichten op te slaan in CouchDB. Dit kan gemakkelijk worden bereikt door middel van een LogListener .
In beide gevallen ontvangt de OSGi LogListener, die binnen de OSGi applicatie ‘leeft’, alle LogEntries die naar de logservice worden verzonden (met wat extra meta-data). Het enige wat de LogListener dan nog hoeft te doen is de conversie naar JSON en het aanmaken van een nieuw document in een database op het CouchDB server. Als men dan de log-berichten zou willen inspecteren, kan een enkele call naar de CouchDB server een replicatie van de database naar uw lokale CouchDB instantie initiëren. Zodra dit gebeurd is, kun je off-line de logs inspecteren.
Installatie en het basis gebruik van CouchDB wordt hier niet beschreven, er zijn al uitstekende beschrijvingen beschikbaar op de Wiki, Halorgium’s GitHub en O’Reilly Free Book .
Om bovengenoemde te testen heb ik een LogListener implementatie gemaakt. Ik gebruik json-simple om een LogEntry te converteren naar JSON en dat resulteert in een JSON Object, zoals bijvoorbeeld:
{
'message': 'This is a test message',
'time': 1269783246909,
'level': 'LOG_DEBUG',
'serviceReference': {},
'bundle': {
'id': 5,
'lastModified': 1269783246510,
'location': 'file:bundle/net.luminis.log.couchdb-1.0.0.jar',
'symbolicName': 'net.luminis.log.couchdb'
}
}
Een unieke serverID/instanceID zou ook kunnen worden toegevoegd om onderscheid te kunnen maken tussen server instanties, mocht je logging van verschillende apparaten naar een centrale server te sturen.
De bovenstaande JSON data POST ik naar de server. Omdat ik zelf geen ‘_id’ in de JSON string heb gedefinieerd, zal CouchDB er een voor mij aanmaken. De HTTP (1.0) POST wordt uitgevoerd door middel van een kale socket naar de CouchDB server.:
POST /db HTTP/1.0
Content-Length: xxx
Content-Type: application/json
{ ... the data here ... }
Het antwoord is ook een geldig JSON object die kan worden gecontroleerd om te kijken of alles is goed gegaan (samen met de HTTP response code, natuurlijk).
In mijn huidige implementatie, kan ik kiezen tussen twee modes; ofwel elk bericht wordt verstuurd naar de CouchDB instantie op een bepaalde tijdstip, of ik stuur alle berichten in bulk-modus elke x seconden. De laatste is waarschijnlijk het beste voor remote couchdb gevallen.
