27.06.2011

Objectify: Persistenz auf Google App Engine

So wünscht man sich eine Persistenz-Bibliothek: lib einbinden, put, delete oder query aufrufen - fertig!
Objectify ist ein sehr dünner Wrapper um die native API des Google Datastore. Damit können nun POJOs und nicht mehr nur Entities gespeichert werden. Keys werden automatisch erzeugt, Transactionen funktionieren wie erwartet und Relationen und mehrwertige Attribute werden auf die offensichtliche Weise angeboten.
Schliesslich können die Objekte aus dem Store 1:1 in GWT genutzt werden.
Wenn also eine Portierung auf JPA oder JDO nicht in Sicht ist und man einfach nur problemlos Daten speichern will: Objectify statt JPA, JDO oder native nutzen!

From: http://ping.fm/s8CFJ

14.06.2011

Google Spreadsheets for iPhone

Did you ever wanted to write a smart App with a cloud-based backend? - Recently I stumbled over the option simply to use a Google Spreadsheet as a data container - at least you have a reasonably stable server and an unbeatably easy maintenance interface. And as I discovered - an API that, despite some lack of documentation, is a fun to use!

Installation

Like all Google Data APIs, the spreadsheet API is a set of well-documented, REST-like, JSON-based services. Simple enough to use. But to be honest - I don't want to bother with JSON objects and REST calls when developing in Objective C for iOS (nor in Java for android).
So - as always, I must say - I was pleased to find a well-maintained iOS library for the Google Data Services called GData Objective C Client. Installation of the library is explained very well here, but does not differ much from other libraries:

  • Checkout from the git repository
  • Drag the framework or the project itself into your project
  • Add the static library as a dependendy
  • include the header files in the search path

Authentication

GData Objective C Client provides services for each Google service as the central point of interaction. In order to use them, include the header file:

#import "GData.h"

Then, for spreadsheets, instantiate the service:

_srv = [[GDataServiceGoogleSpreadsheet alloc] init];
[_srv setShouldCacheResponseData:YES];
[_srv setServiceShouldFollowNextLinks:YES];
[_srv setUserCredentialsWithUsername:username
                            password:password];


As you can see, here we send specific user credentials. This approach is not optimal since they have to be stored locally by your App. Google favours OAuth and even considers the explicit approach deprecated. Maybe in a later post, I will discuss how to use OAuth.

Where are my spreadsheets?

Having a service at hand, we can now start querying our document repository for spreadsheets:

NSString *serviceRootURL = [GDataServiceGoogleSpreadsheet serviceRootURLString];
NSString *sheetsQuery = @"spreadsheets/private/full";
NSString *query = [serviceRootURL stringByAppendingString:sheetsQuery];
NSURL *feedURL = [NSURL URLWithString:query];
SheetFeedDelegate *wrapper = [[SheetFeedDelegate alloc] initWithDelegate:delegate];
[_srv fetchFeedWithURL:feedURL
              delegate:wrapper
     didFinishSelector:@selector(ticket:finishedWithFeed:error:)];

As you can see, the google data services work feed-based: You request a specific URL and asynchroneously receive the feed as a result. Thus we first have to construct the URL from a service base URL an a query part, "spreadsheets/private/full". "spreadsheets" denotes what you are asking for, "private" means send me all spreadsheets available for the account  I'm authenticated for ("public" would show you only public sheets). "full", for the spreadsheet feed is the usual option (on cell feeds, "values" or "basic" would only give you values, not formulas and leave you in a readonly state).

Since the receipt of the result happens asynchroneously, you have to provide a delegate, on which a given selector is applied (you could also use Objective-C blocks if you prefer them):

- (void)ticket:(GDataServiceTicket *)ticket
  finishedWithFeed:(GDataFeedSpreadsheet *)feed
             error:(NSError *)error {
    if (!error) {
        NSMutableArray *result = [[NSMutableArray alloc] init];
        for (GDataEntrySpreadsheet *entry in [feed entries]) {
            GDataTextConstruct *title = [entry title];
            Sheet *sheet = [[[Sheet alloc] 
                            initWithTitle:[title stringValue]
                            worksheetsFeed:[entry worksheetsFeedURL]] 
                            autorelease];
            [result addObject:sheet];
        }
        [_delegate foundSheets:result];
    } else {
        NSLog(@"Error on load");
    }
}

Here, feed is the full result.  In this example, a list of Sheet objects is created to hold the result. Their definition is not interesting, but pay attention on how the title of the spreadsheet is returned via a GDataTextConstruct. Moreover, you see the worksheetsFeedURL being stored in the temporary structure. This is a general pattern: Items returned by the Google services often contain URLs to other feeds enabling further related queries and even modifications without further manual URL construction. It is often advisable to store these URLs locally for further use.

Get Worksheets

Having the worksheets feed available, querying the spreadsheed for its worksheets is already a well-known operation:


[_srv fetchFeedWithURL:sheet.worksheetsFeed
              delegate:wrapper
     didFinishSelector:@selector(ticket:finishedWithFeed:error:)];


With a delegate that does again:


- (void)ticket:(GDataServiceTicket *)ticket
finishedWithFeed:(GDataFeedSpreadsheet *)feed
           error:(NSError *)error {
    if (!error) {
        NSMutableArray *result = [[NSMutableArray alloc] init];
        for (GDataEntryWorksheet *entry in [feed entries]) {
            GDataTextConstruct *title = [entry title];
            Worksheet *worksheet = [[[Worksheet alloc] 
                                    initWithSheet:_sheet 
                                    listFeed:[[entry cellsLink] URL]
                                    title:[title stringValue]]
                                    autorelease];
            [result addObject:worksheet];
        }
        [_delegate foundWorksheets:result];
    } else {
        NSLog(@"Error on load");
    }
}


In this code, you see only one single change: From the GDataEntryWorksheet, we do not store the listFeedURL as we did for the worksheet feed, but we use the cellsLink. See the next section for a discussion of the difference.

Get Cells

When it comes to query for actual worksheet cells, you have the choice: Either you use a list-based or a cell-based feed.
  • The list-based feed works best when you worksheet can be seen just like a database table: With defined column and headers in the first row. For each subsequent row, you will get a set of column values. Sounds familiar.
  • The cell-based feed is a more general approach and gives you the freedom to query any row-column-area of your worksheet.
Since we already coped with list feeds let's use a cell-based feed now. Therefore we stored the cellsLink above which contains the URL of a cell-based feed but by default is queried exactly like a list-based feed (though you could also use eg fetchFeedWithQuery to make more concrete requests):
[_srv fetchFeedWithURL:worksheet.listFeed
              delegate:wrapper
     didFinishSelector:@selector(ticket:finishedWithFeed:error:)];
Lets take a look at the delegate again:
- (void)ticket:(GDataServiceTicket *)ticket
finishedWithFeed:(GDataFeedSpreadsheetCell *)feed
         error:(NSError *)error {
    if (!error) {
        NSInteger rows = [feed rowCount];
        NSInteger cols = [feed columnCount];
        NSMutableArray *result = [[NSMutableArray alloc] init];
        for (int i=0; i<rows; i++) {
            Row *row = [[Row alloc] initWithWorksheet:_worksheet columnCount:cols];
            [result addObject:row];
        }
        for (GDataEntrySpreadsheetCell *entry in [feed entries]) {
            GDataSpreadsheetCell *cell = [entry cell];
            NSInteger rowNo = [cell row]-1;
            NSInteger colNo = [cell column]-1;
            [self setEntry:entry forRow:rowNo forColumn:colNo];
        }

        [_delegate foundList:result];
    } else {
        NSLog(@"Error on load");
    }
}
As you of course have noticed, we did not extract actual cell values until now. We only build an a row/column-based index of objects of type GDataEntrySpreadsheetCell. What if you want to know the actual value of a cell?
-(NSString*) getValueForRow:(int)row forColumn:(int)column {
    GDataEntrySpreadsheetCell *entry = [self getEntryForRow:row forColumn:column];
    GDataSpreadsheetCell *cell = [entry cell];
    return [cell resultString];
}
So... why this indirection? The GDataEntrySpreadsheetCell is quite a huge structure so the ideal solution would be stored the string value only. But then, updating cells would get a little trickier, as I'll explain in the next section:

Update Cells

Updating a cell is more or less trivial when you have the entry at hand:
[_srv fetchEntryByUpdatingEntry:entry
                           delegate:delegate
                  didFinishSelector:@selector(ticket:finishedWithFeed:error:)];
where the delegate is simply implemented like:
- (void)ticket:(GDataServiceTicket *)ticket
finishedWithFeed:(GDataEntrySpreadsheetCell *)entry
         error:(NSError *)error {
    [self setEntry:entry forColumn:_column];
}
Huh? - Yes, you have seen this line of code earlier, when we retrieved cell values from the server. It is important to store the new entry object sent as a response to your update request. If you don't, upon sending the next update request, Google will detect an intermediate change and send you a corresponding error.

So updating a cell value is easy if you store the corresponding entry object. For less memory footprint though, you can store only the value and re-create the entry from scratch. The only thing you have to do is also to store the so-called etag from the entry and assign it to the new entry so that google again can check for intermediate changes.

These are the simpliest operations available via the Google Data API for Spreadsheets. From this starting point, the design of advanced apps using spreadsheets should be possible.

ScrumMaster: Me too!

Jetzt bin ich also auch ScrumMaster. Habe zumindest die passende Schulung hinter mir. Bei Boris Gloger, nach eigener Aussage einer der ersten seiner Art. Was hats mir gebracht? Was ändert sich?

Scrum ist ja heute Commodity, state of the art, fast schon langweilig, auf jeden Fall muss man aber danach arbeiten. Jedenfalls in der agilen Web-Szene. Erklären braucht man wohl nicht mehr. Alle kennen die Stichwörter von Estimation bis Retrospective Meeting, von Product Owner bis Team Member und von Task Board bis Burndown Chart. Dennoch: Jenseits dieser formalisierten Prozessbestandteile: Was ist der Kern? Was muss man verinnerlichen? Was ist wirklich das Mindset?

Natürlich: Eine Schulung macht noch keinen Master. Da werde ich wohl das eine oder andere Projekt... stop, schon der erste Fehler. Also das ein oder andere Produkt betreuen und ein paar Sprints mitmachen müssen.

Aber die Grundidee, die ist nicht so neu: Softwaresysteme werden sehr schnell sehr kompliziert. Oft viel zu kompliziert für die Benutzer, die manchmal mit sehr viel weniger Funktionen auskommen würden. Die richtigen Features erkennt man aber viel leichter, wenn man schon mit dem System arbeiten kann. Sie abstrakt anhand einer Anforderungsbeschreibung herauszulesen, ist anspruchsvoll.

Also: Gar nicht das komplette System erstellen, sondern den kleinsten nutzbaren Teil davon. Dann die kleinste mögliche Erweiterung. Und so weiter. Und darauf spekulieren, dass irgendwann der Funktionsumfang schon als ausreichend beurteilt wird, obwohl im Fachkonzept vielleicht noch ein paar hundert Anforderungen stehen.

Ausserdem ist dann schon sehr früh klar, wie gut Schätzung und Realität übereinstimmen. Dann kann man rasch gegensteuern (das muss dann aber auch passieren!). Ach ja, die Schätzung - da gibts eben die beiden Tricks: Nicht schätzen, wie lange etwas braucht, sondern was man in einer gegebenen Zeit schafft. Und schätzen durch Vergleich zweier Aufgaben.

Und der Rest? - Entwickler direkt mit den Benutzern (oder Product Ownern) reden lassen, keine Stille Post. Ein Team durch Commitment bei der Ehre packen. Den Product Owner die Probleme der Entwickler spüren lassen. Viele gut durchdachte Tips für den alltäglichen Arbeitstalltagswahnsinn.

Das ist für mich Scrum. Doch, schon - ich freue mich auf das nächste Projekt - äh, Produkt.