[Tutorial für fortgeschrittene] Android an eine PHP Rest Api anbinden

  • Antworten:4
Pascal P.
  • Admin
  • Forum-Beiträge: 11.286

28.08.2016, 12:45:44 via Website

Hallo zusammen,

Für Leute die kein Vorwort lesen wollen

Hier im Forum kommen immer wieder Fragen zur Server Anbindung mit Android vor,
daher dachte ich mir, ich erstelle ein Tutorial, wie ich meine Apps mit meinem Server kommunizieren lasse.

Im Wesentlichen soll es hier um die API Programmierung in PHP gehen, aber auch wie man diese API auf Android Seite gut benutzt.
Dabei lege ich großen Wert aufs Error Handling, welches hier auch durchgereicht wird.

(!)WICHTIG: Dieses Tutorial richtet sich an mittelmäßig- bis gute Android Programmierer. Um das hier zu verstehen benötigt man vertieftes wissen in Java und basiswissen in PHP. Bitte vor der Verwendung unbedingt lesen, denn mit Copy&Pase werdet ihr (hoffentlich) nicht weit kommen ;)

Wer nicht mehr so richtig weiß was REST und das Format JSON ist bitte nachlesen.

Als Beispiel programmiere ich einen (wirklich) kleinen MySql Client für Android, für MySql Datenbanken welche über Port 3306 nicht von außen aus dem Internet erreichbar sind.

[Das Endprodukt in PHP und Android ist zwar auf GitHub vorhanden, bevor es jedoch passend genutzt werden kann, muss erst eine passende API Sicherung/Login eingebaut werden, ansonsten kann JEDER eure DB lesen schreiben und löschen. Deswegen läuft das ganze bei mir lokal im Netzwerk und mit einer Test DB.

Ich benutze folgendes:
1. Einen PHP fähigen Server (lokal XAMPP) (ab PHP 5.1.x)
2. Einen gescheiten PHP Codeditor ( Notepad++ oder VisualStudio Code)
3. AndroidStudio für die App

Warum ein extra Tutorial, kann es nicht jeder so machen wie er will?
Natürlich kann es jeder nach Lust und Laune programmieren, ich will hier nur mal meine Vorgehensweise beschreiben, welche dem einen oder anderen durchaus helfen kann.

Seitdem ich mit Android und PHP arbeite, erstelle ich gelegentlich APIs um irgendwelche Daten vom Server abzurufen oder zu speichern.
Am Anfang ganz primitiv, jede „Funktion“ ist eine PHP Datei, und die Parameter per get und Post übertragen.
Wenn man damit jetzt einen größeren Funktionsumfang in der API anbieten will, hat man recht schnell viele PHP Dateien und es wird unübersichtlich, hinzu kommt noch das man viele recht unschöne Urls hat
z.b. http://example.com/api/getPersons.php?offset=10&limit=10

Diese URL gibt mir z.b. 10 Personen aus einer Datenbank zurück, 10 an der Zahl ab dem 10 Eintrag.
Ich Persönlich mag diese Art API URLs nicht, und es ist auch kein Standard.
Schaut man sich andere APIs an und setzt den Standard um dann müsste die URL gewisser maßen so aussehen:
http://example.com/api/persons/10/10
Da habe ich keine PHP Extension und kein Filename mehr.

Genau das war dann auch mein Ziel, weg von den einzelnen Dateien, hin zu den Standard API URLs.
Aber wie umsetzten ohne Grundlage – das war die Frage.
Ich brauchte eine Art allgemeine API Definition, welche sich beliebig erweitern lässt.

Gedacht getan, aus dieser Idee entstand meine PHP Klasse ApiFrame.class.php.
Damit habe ich alles was ich brauche, eine allgemeine Klasse welche API aufrufe entgegennimmt, auswertet und im PHP Skript selber an die entsprechende Funktion vermittelt und dabei den Rückgabewert in JSON ausgibt.

Den Aufbau der Klasse und die einzelnen Methoden werde ich jetzt nicht im Detail erklären, wenn es jemand interessiert, bitte nicht scheuen nachzufragen.
Auf jeden Fall dachte ich mir dann, wenn schon so allgemein, dann auch noch allgemeine Infos wie Status und Server Verarbeitungszeit und Errors einbauen.
Damit war die Grundlage gesetzt.

Nun kann man mit dieser Klasse ganz einfach APIs erstellen, zudem ist man nicht mehr an einzelne Dateien gebunden, sondern kann viele API Funktionen in einer Datei definieren.

— geändert am 28.08.2016, 12:48:37

LG Pascal //It's not a bug, it's a feature. :) ;)

swa00Ludy

Antworten
Pascal P.
  • Admin
  • Forum-Beiträge: 11.286

28.08.2016, 12:46:02 via Website

Benutzung des ApiFrame
[[Falls die längeren Codeaussschnitte im folgenden schlecht zu lesen sind wegen Formatierung etc. könnt ihr auch bei GitHub reinschauen, da behält man die Übersicht]]
Prinzip der Definition ist relativ einfach:
Auszug aus so einer Definition mit anschließender Methode:

/**
Get all Databases
@get db/all
*/
 function getDatabases($frameObject)
 {
         $frame = Frame::get($frameObject);
        $pdo = initPdo();//Init pdo db connection
        $res = executeStatement("Show Databases");//execute select
      $frame->output($res);//Set db result into frame
  } 

Wie wir sehen definieren wir die relative URL mit

@get [Pfad]

Wobei der Pfad Text und Platzhalter für Parameter sein kann.
Bsp. nur text: db/all
Bsp. mit Platzhalter: db/{databaseName}/tables`

Und erweiterbar mit RegEx zum Prüfen der Eingabe falls erforderlich:
db/{databaseName:“/^[0-9]$/“}/tables
(RegEx mach in diesem Fall keinen Sinn, nur als Beispiel)

Dies sind die URL Definitionen, wie man eine API URL definieren kann.
Desweitern haben wir ja noch GET und POST Parameter die trotzdem vorhanden sein können.
Beispiel für eine GET Browser url: http://example.com/api/db/test/tables?where=id=12
Da haben wir trotz Platzhalter in der URL noch ein where im GET.

Das können wir so definieren:
Syntax: @param [name] method=“[get|post]“ [pattern=“..“]– optional
Beispiel:

@get db/{databaseName}/tables
@param where method=“get“

Ähnlich verhält es sich mit Post Parametern:

@param [name] method=“post“ [pattern=“…“]

Damit haben wir unsere Methodendefinition beendet.

Eine Methodendefinition steht immer eingeleitet von einem Kommentar (/**) über der entsprechenden „Web“ Methode. Wie diese dann heißt ist egal, ausschlaggebend ist die Definition im Methodenkommentar.

Wichtig dabei ist, dass die Methode die Parameter auch alle akzeptiert, d.h. wenn 4 oben definiert sind, muss die Methode auch immer 4 Parameter aufnehmen.
Wobei ein zusätzlicher Parameter immer das FrameObjekt selber ist, welches man übergeben bekommt (immer als letzten Parameter)
Sobald das definiert ist, kann eine Test Methode geschrieben werden.
Ein solches PHP Skript sieht z.b. dann so aus:

<?php
require_once("ApiFrame.class.php"); //Import ApiFrame script
$frame = new Frame();//Init a new instance
$frame->Start();//Frame starten
$frame->enableErrorReporting(false);//Globale errors abfangen, Parameter nur true wenn auch errors abgefangen werden sollen, ohne nur FatalErrors und exceptions
/**
This commtent defines a test API method
@get api/{id:"/^[0-9]*$/"}/{name}
@param text method="post"
*/
 function testAPI($id,$name,$text,$frameObject)
 {
  $frame = Frame::get($frameObject); // Cast frame object
  $arr = array("id"=$id,"name"=>$name,"text"=>$text); //Schreibe Parameter in ein Array
      $frame->output($arr);//Gebe das array aus
    $frame->setHttpStatusCode(200); // Setze http status code [bei 200 überlüssig]
 }
$frame->handleApiUrls(__FILE__,$_GET["__url"]);//Behandle API urls benutze für die URL zuordnung den angegebenen GET Parameter [htaccess redirect] 

?>

Wie man sehen kann muss man im Endeffekt nur die APIFrame Klasse starten, der Rest passiert von selber, der Output innerhalb der Methode muss halt noch gesetzt werden, aber sonst, ist das Handling übersichtlich, wenn man sich nicht gerade den Code der Frame Klasse selber anschaut ;).

Wenn man das jetzt aufrufen würde wäre es nur über eine URL a la:

http://example.com/api/frameTest.php?__url=api/110167/PlPt

erreichbar.[Post Parameter hier mal außer Acht gelassen].
Damit wir diese URL nachher richtig haben, fügen wir entsprechende Zeilen in die .htaccess ein, dazu aber später mehr.

Schauen wir uns erstmal den JSON Output an, welcher ungefähr so aussieht:

{"data":{"id":"110167","name":"Test","text":"Hello My Friend"},"status":"OK","elapsedTime":0.0013339519500732,"errorData":null}

Nun sehen wir, dass unser ApiFrame um unsere Ausgabe wirklich einen Frame mit ein paar Infos generiert. Hier stehen uns der Status, [Server]Errors die Daten und die Serververarbeitungszeit zur Verfügung.
Das Beste ist, Errors:

Es werden sowohl Exceptiones vom User aus dem PHP verarbeitet als auch PHP Errors wie Compilerfehler, null Pointer Time-outs etc..

Damit ist bei einem Fehler die API nicht einfach „tot“ sondern meldet zurück was für ein Fehler gerade vorliegt (sofern möglich und kein Fatal error o.ä., jeder Error wird abgefangen)

Soweit zur allgemeinen Verwendung. Hier mal meine API File zur kleinen phpMyAdmin App:

<?php
require_once("../classes/ApiFrame.class.php");
require_once("DataTable.class.php");
$frame = new Frame();
$frame->Start();
$frame->debug = false;
ini_set('memory_limit', '-1');
$frame->enableErrorReporting(false);

/**
Get all Databases
@get db/all
*/
 function getDatabases($frameObject)
 { 
     $frame = Frame::get($frameObject);
     $pdo = initPdo();


    $res = executeStatement("Show Databases");
    $frame->output($res);    
 }

 function executeStatement($sql,$pdo=null)
 {    if($pdo==null)$pdo = initPdo();

    $stmt = $pdo->query($sql);
              $rows = $stmt->fetchAll(PDO::FETCH_ASSOC);
           $tbl = new DataTable($rows,$sql,$stmt);
      return $tbl;
 }

  function initPdo()
  {
      $server   = 'mysql:host=localhost;charset=utf8';
    $user     = 'myuser';
    $password = 'mypass';



    $options  = array
                (
                  PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8',

                );
    $pdo      = new PDO($server, $user, $password, $options);
    $pdo->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
    return $pdo;
  }

 /**
Get all tables with the database contains
@get db/{name}/tables
*/
function getTablesFromDatabase($name,$frameObject)
{
    $frame = Frame::get($frameObject);
    $res = executeStatement("Show tables from " . $name);
      $frame->output($res);
}

 /**
Execute a Query in a specific database context
@get db/{name}/query
@param sql method="post" pattern="/^(?!.*Delete).*$/"
*/
function execQuery($name,$sql,$frameObject)
{
    $frame = Frame::get($frameObject);
    $pdo = initPdo();
    $pdo->query("USE $name;");
    $res = executeStatement($sql,$pdo);
           $frame->output($res);
}
$frame->handleApiUrls(__FILE__,$_GET["__url"]);
?>

Die executeStatment funktion wertet die PDO results aus und schreibt diese in ein DataTable Objekt:

<?php
class DataTable
{
    public $sql;
    public $columns;
    public $rows;
    public $rowCount;
    public $columnCount;
    function  __construct($result,$sql,$stmt)
    {
        $this->sql = $sql;
        $this->columns = $this->getColumns($result);
        $this->rows = $this->getRows($result);
        $this->rowCount = $stmt->rowCount();
        $this->columnCount = count($this->columns);
    }   
    function getColumns($result)
    {
        $clm = array();
        foreach($result[0] as $key => $value)
        {
            $clm[] = $key;
        }

        return $clm;

    }   

    function getRows($result)
    {
        $out = array();
        foreach($result as $res)
        {

            $out[] = array_values($res);

        }

        return ($out);
    }

}


?>

Da wir die API Funktionen an sich haben, geht es jetzt zur Einrichtung mit der HTACCESS:
Hier müssen wir ein sogenanntes alias(URL_MOD_REWRITE) verwenden, damit können wir die URL ändern und im Hintergrund zeigt diese aber trotzdem auf unsere PHP Datei mit dem Parameter __url als weiterführenden Pfad.

RewriteEngine on 
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^api/(.+)?$  index.php?__url=$1& [QSA]

Damit wäre es erledigt, diese htaccess muss im selben Ordner wie unsere PHP Datei (in diesem Fall ist die API Datei index.php genannt) liegen, damit die Regel greift.
Jetzt ist unsere API so abrufbar:
http://www.example.com/api/db/all

— geändert am 28.08.2016, 12:57:38

LG Pascal //It's not a bug, it's a feature. :) ;)

Ludy

Antworten
Pascal P.
  • Admin
  • Forum-Beiträge: 11.286

28.08.2016, 12:46:19 via Website

Programmieren der Client App
Da wir nun die API haben geht es an die App.
Hier ist wichtig, dass man schon gute Java Kenntnisse hat, denn aufgrund des ApiFrames müssen wir mit typisierten Objekten arbeiten, wenn wir Objektorientiert bleiben wollen.
Ich werde hier auch nur das wichtigste den Klassenaufbau und Benutzung erklären und nicht wie man sowas im Detail aufbaut.
Da haben wir eine Klasse ApiFrame wobei das T für den entsprechenden Datentyp steht welcher im Attribut „data“ ausgegeben wird. (Typisiertes Objekt)

Geben wir unserem PHP als Output ein PHP Objekt mit, können wir das in Java auch wieder leicht in ein Java Objekt deserialisieren und müssen uns nicht um die Deserialisierung mit Keys kümmern.
Das wichtige dabei ist, dass wir die Java Klasse genauso erstellen (mit den gleichen öffentlichen Feldern) wie die PHP Datei, denn sonst gehen Daten verloren.
Hier erst einmal die Klasse ApiFrame.

package de.plpt.phpmyadmin; 

import com.google.gson.Gson; 
import com.google.gson.JsonObject; 
import com.google.gson.annotations.SerializedName; 
import com.google.gson.reflect.TypeToken; 

import java.lang.reflect.Type; 

/** 
 * Created by Pascal on 17.04.2016. 
 */ 
public class ApiFrame<T> { 

    //region VarDefs 
    @SerializedName("data") 
    private T data; 

    @SerializedName("status") 
    private String status; 

    @SerializedName("elapsedTime") 
    private Double elapsedTime; 

    @SerializedName("errorData") 
    private ErrorData errorData; 
    //endregion 

    //region Constructor 
    public ApiFrame(JsonObject result,Type cls) { 
        Type collectionType = new TypeToken<ApiFrame<T>>() { 
        }.getType(); 


        ApiFrame<T> userApiFrame = (ApiFrame<T> ) new Gson().fromJson(result, collectionType); 


        if(!userApiFrame.hasErrorData()) 
        this.data = new Gson().fromJson(result.getAsJsonObject("data"), cls);//Parse data separate because of NullNode and JAva Generic var as Object and not as Type 
        this.status = userApiFrame.status; 
        this.elapsedTime = userApiFrame.elapsedTime; 
        this.errorData = userApiFrame.errorData; 

    } 
    //endregion 


    //region VarGetters 
    public String getStatus() { 
        return status; 
    } 

    public Double getElapsedTime() { 
        return elapsedTime; 
    } 

    public ErrorData getErrorData() { 
        return errorData; 
    } 

    public boolean hasErrorData() 
    { 
        return errorData!=null; 
    } 

    public T getData() { 
        return data; 
    } 
    //endregion 


} 

    class ErrorData 
    { 
        //region VarDefs 
        @SerializedName("errorCode") 
        private int  errorCode; 

        @SerializedName("errorDetails") 
        private String  errorDetails; 


        @SerializedName("errorMessage") 
        private String  errorMessage; 


        @SerializedName("function") 
        private String  function; 


        @SerializedName("information") 
        private String  information; 
        //endregion 

        @Override 
        public String toString() 
        { 
            return String.format("(%s) %s  /   %s",errorCode,errorDetails,information); 
        } 

    } 

Wenn man sich nun diese APIFrame Klasse anschaut, bietet diese uns nur die „leere Hülle“ für useren Request bzw. unsere Daten.
Über die SerializedName Annotations kann die Google Gson lib eine Instanz der Klasse aus einem JsonString erzeugen.
Damit können wir dynamisch jede beliebige Klasse über unseren Frame übertragen, denn wir haben ja ein Typisiertes Objekt.

Nachdem wir die allgemeine FrameKlasse haben, müssen wir uns jetzt um die eigentliche Datenklasse der DataTable.java kümmern.
Da gibt es jetzt zwei Möglichkeiten:

  1. Wir schreiben die PHP Klasse ab in Java und fügen unsere Annotations etc. hinzu
  2. Oder wir sind faul, rufen die API Url auf, kopieren das Output Json und lassen und den Java Code z.b. hier erstellen: http://www.jsonschema2pojo.org/

Dann müssen wir nur noch aus dem generierten die entsprechende Klasse (generiert Data genannt, herauskopieren und etwas anpassen)

Am Ende sollte eine Datenklasse so aussehen. Habe diese Klasse schon um weitere Methoden erweitert um damit gescheit arbeiten zu können (z.b. Iteration):

package de.plpt.phpmyadmin; 

import com.google.gson.annotations.Expose; 
import com.google.gson.annotations.SerializedName; 
import com.google.gson.internal.Streams; 

import java.io.Serializable; 
import java.util.ArrayList; 
import java.util.Iterator; 
import java.util.List; 

/** 
 * Created by Pascal on 29.07.2016. 
 */ 
public class DataTable implements Serializable,Iterable<List<String>> { 


    @SerializedName("sql") 
    @Expose 
    private String sql; 
    @SerializedName("columns") 
    @Expose 
    private List<String> columns = new ArrayList<String>(); 
    @SerializedName("rows") 
    @Expose 
    private List<List<String>> rows = new ArrayList<List<String>>(); 
    @SerializedName("rowCount") 
    @Expose 
    private Integer rowCount; 
    @SerializedName("columnCount") 
    @Expose 
    private Integer columnCount; 

    /** 
     * 
     * @return 
     * The sql 
     */ 
    public String getSql() { 
        return sql; 
    } 

    /** 
     * 
     * @param sql 
     * The sql 
     */ 
    public void setSql(String sql) { 
        this.sql = sql; 
    } 

    /** 
     * 
     * @return 
     * The columns 
     */ 
    public List<String> getColumns() { 
        return columns; 
    } 

    /** 
     * 
     * @param columns 
     * The columns 
     */ 
    public void setColumns(List<String> columns) { 
        this.columns = columns; 
    } 

    /** 
     * 
     * @return 
     * The rows 
     */ 
    public List<List<String>> getRows() { 
        return rows; 
    } 

    /** 
     * 
     * @param rows 
     * The rows 
     */ 
    public void setRows(List<List<String>> rows) { 
        this.rows = rows; 
    } 

    /** 
     * 
     * @return 
     * The rowCount 
     */ 
    public Integer getRowCount() { 
        return rowCount; 
    } 

    /** 
     * 
     * @param rowCount 
     * The rowCount 
     */ 
    public void setRowCount(Integer rowCount) { 
        this.rowCount = rowCount; 
    } 

    /** 
     * 
     * @return 
     * The columnCount 
     */ 
    public Integer getColumnCount() { 
        return columnCount; 
    } 

    /** 
     * 
     * @param columnCount 
     * The columnCount 
     */ 
    public void setColumnCount(Integer columnCount) { 
        this.columnCount = columnCount; 
    } 

    public String getValue(int row,int column) 
    { 
        return rows.get(row).get(column); 
    } 

    public String getValue(int row,String columnName) 
    { 
        int idx = columns.indexOf(columnName); 
        return getValue(row,idx); 
    } 

    @Override 
    public Iterator<List<String>> iterator() { 
        return rows.iterator(); 
    } 



} 

Die @.. Attribute über den Variablen die die Google Gson anotations und helfen der Gson lib von google beim automatischen deserialisieren des Json Strings.

Mit den beiden Klassen können wir jetzt ein Objekt vom Typ ApiFrame erstellen, welches uns erlaubt auf die Daten des Frames und die der DataTable zuzugreifen.

Würde ich im PHP jetzt eine Personen Klasse definiert und ausgegeben haben, dann könnte ich auch sowas machen: ApiFrame würde genauso funktionieren, vorausgesetzt man hat alle Felder richtig genannt etc.
Da wir jetzt auch unsere Java Klassen fertig haben, geht es jetzt an die eigentliche App.
Für den http teil verwende die die http Lib Ion, es geht aber mit etwas Umbau auch jede andere http(s) Lib.
Außerdem könnte man hier gut einen RestClient wie Retrofit verwenden, da ich mit diesem aber nicht nie gearbeitet habe, kann ich darüber keine Aussage machen und auch hier nicht vorstellen.
Wie immer bei http Clients brauchen wir einen Callback.

LG Pascal //It's not a bug, it's a feature. :) ;)

Ludy

Antworten
Pascal P.
  • Admin
  • Forum-Beiträge: 11.286

28.08.2016, 12:46:33 via Website

Beispielaufruf aus einer Activity
In diesem Beispiel benutze ich im ApiFrame(Request) den Ion Callback und nach außen hin meinen eigenen damit ich direkt im Callback mein ApiFrame Objekt zur Verfügung habe und es benutzen/weiterverarbeiten kann.
Um nun auch Request absetzen zu können, brauchen wir noch eine Hilfsklasse, welche uns die Aufrufe kapselt.
Am Ende soll die API aus einer Activity o.ä. so aufrufbar sein:

ApiRequest.newInstance(MainActivity.this).getTables(„testDBName“,new ApiRequest.ApiResponse() {
    @Override
    public void onFinish(Exception x, ApiFrame<?> frame) {
        if(x!=null)
        {
            x.printStackTrace(); //Error
            return;
        }
        ApiFrame<DataTable> tblFrame = (ApiFrame<DataTable>;)frame;
    //mit dem Typisierten Frame weiterarbeiten
if(!tblFrame.hasErrorData())
{
//Kein Error, Daten verarbeiten, anzeigen etc..

}
else
{
    //handle frame error
}

});

LG Pascal //It's not a bug, it's a feature. :) ;)

Ludy

Antworten
Pascal P.
  • Admin
  • Forum-Beiträge: 11.286

28.08.2016, 12:47:02 via Website

Technischer Aufbau der Request Klasse
Um das so „schön“ hinzubekomen brauchen wir die Hilfsklasse ApiRequest. In dieser wird der eigentliche http Request geschickt und die Antwort dann verarbeitet und in den API Frame geparst.
Dort sind auch die URLs (base und Relativ) Definiert. Um die Strings nicht (komplett) Hardcoden zu müssen, habe ich mir dafür eine eigene Annotation Klasse (Interface) erstellt:

@Retention(RetentionPolicy.RUNTIME)
public @interface ApiUrl {
    String URL();
    String BaseUrl() default "";
}

Und die eigentliche Klasse:

package de.plpt.phpmyadmin; 

import android.content.Context; 

import com.google.gson.JsonObject; 
import com.koushikdutta.async.future.FutureCallback; 
import com.koushikdutta.ion.Ion; 

import java.lang.reflect.Method; 
import java.util.ArrayList; 
import java.util.HashMap; 
import java.util.List; 

/** 
 * Created by Pascal on 30.07.2016. 
 */ 
@ApiUrl(BaseUrl = "http://192.168.0.7/apis/phpMyAdmin/api",URL="") 
public class ApiRequest { 

//region InterfaceDefinition 
public interface  ApiResponse 
{ 
    void onFinish(Exception x,ApiFrame<?> frame); 
} 
//endregion 

//region VarDefs 
private Context ctx; 
//endregion 

//region Constructor and Static constructor 
public ApiRequest(Context c) 
{ 
    this.ctx = c; 
} 

public static ApiRequest newInstance(Context ctx) 
{ 
    return new ApiRequest(ctx); 
} 
//endregion 

//region public API Methods 

/** 
 * Get (list) all DataBases on a server 
 * @param res ApiResponse Callback 
 */ 
@ApiUrl(URL="/db/all") 
public void getDatabases(final ApiResponse res) 
{ 
    String url = buildUrl(getClassAnnotation().BaseUrl(),getAnnotation().URL()); 
    Ion.with(ctx).load(url).asJsonObject().setCallback(new FutureCallback<JsonObject>() { 
        @Override 
        public void onCompleted(Exception e, JsonObject result) { 

            if(e!=null) 
            { 
                res.onFinish(e,null); 
                return; 
            } 
            ApiFrame<DataTable> userApiFrame = new ApiFrame<DataTable>(result, DataTable.class); 
            res.onFinish(null,userApiFrame); 
        } 

    }); 

} 

/** 
 * Get (list) the tables for a specific database 
 * @param dbName databaseName 
 * @param res ApiResonse Callback 
 */ 
@ApiUrl(URL="/db/%s/tables") 
public void getTables(String dbName,final ApiResponse res) 
{ 
    String url = buildUrl(getClassAnnotation().BaseUrl(),getAnnotation().URL(),dbName); 
    Ion.with(ctx).load(url).asJsonObject().setCallback(new FutureCallback<JsonObject>() { 
        @Override 
        public void onCompleted(Exception e, JsonObject result) { 

            if(e!=null) 
            { 
                res.onFinish(e,null); 
                return; 
            } 
            ApiFrame<DataTable> userApiFrame = new ApiFrame<DataTable>(result, DataTable.class); 
            res.onFinish(null,userApiFrame); 
        } 

    }); 

} 

/** 
 * Executes a SQL Query 
 * @param dbName Database name to execute 
 * @param query Query string 
 * @param res ApiResponse Callback 
 */ 
@ApiUrl(URL="/api/%s/query") 
public void query(String dbName,String query,final ApiResponse res) 
{ 
    String url = buildUrl(getClassAnnotation().BaseUrl(),getAnnotation().URL(),dbName); 

    Ion.with(ctx).load(url).addHeaders(createPostParameters(new String[]{"sql",query})).asJsonObject().setCallback(new FutureCallback<JsonObject>() { 
        @Override 
        public void onCompleted(Exception e, JsonObject result) { 

            if(e!=null) 
            { 
                res.onFinish(e,null); 
                return; 
            } 
            ApiFrame<DataTable> userApiFrame = new ApiFrame<DataTable>(result, DataTable.class); 
            res.onFinish(null,userApiFrame); 
        } 

    }); 

} 

//endregion 


//region privateMethods 

/*** 
 * Builds the Url from Base and relative Strings from annotation 
 * @param base Base url 
 * @param relativ relative url 
 * @param params formatter params 
 * @return complete url string 
 */ 
private String buildUrl(String base,String relativ,Object... params) 
{ 
    return base + String.format(relativ,params); 
} 

/*** 
 * Returns the current Method Annotation 
 * @return current Method (above) Annotation 
 */ 
private ApiUrl getAnnotation() { 
    //Method m = this.getClass().getEnclosingMethod(); 


    Method m = null; 
    try { 
        m = getCallingMethod(); 
    } catch (ClassNotFoundException e) { 
        e.printStackTrace(); 
    } 

    if(m.isAnnotationPresent(ApiUrl.class)) 
    { 
        ApiUrl ta = m.getAnnotation(ApiUrl.class); 
        return ta; 
    } 

    return null; 

} 

/** 
 * Creates a HashMap for Parameters oput of a String array 
 * @param params paramters to process 
 * @return HashMap of KeyValue pairs for use for ION http post 
 */ 
private static HashMap<String,List<String>> createPostParameters(String[]... params) { 

    HashMap<String,List<String>> map = new HashMap<>(); 

    for(String[] arr : params) { 
        List<String> l = new ArrayList<String>(); 

        l.add(arr[1]); 
        map.put(arr[0],l); 
    } 

    return map; 

} 

/*** 

     * Returns the MethodObject for the current Calling Method (2 steps above) 
     * @return caller Method object 
     * @throws ClassNotFoundException exception if something goes wrong... 
     */ 
    private static Method getCallingMethod() throws ClassNotFoundException{ 
        final Thread t = Thread.currentThread(); 
        final StackTraceElement[] stackTrace = t.getStackTrace(); 
        final StackTraceElement ste = stackTrace[2+2]; 
        final String methodName = ste.getMethodName(); 
        final String className = ste.getClassName(); 
        Class<?> kls = Class.forName(className); 
        do{ 
            for(final Method candidate : kls.getDeclaredMethods()){ 
                if(candidate.getName().equals(methodName)){ 
                    return candidate; 
                } 
            } 
            kls = kls.getSuperclass(); 
        } while(kls != null); 
        return null; 
    } 

    /*** 
     * Gets the Annotation for Specific Class 
     * @return Annotation of specific Class 
     */ 
    private ApiUrl getClassAnnotation() 
    { 
        if(this.getClass().isAnnotationPresent(ApiUrl.class)) 
        { 
            ApiUrl ta = this.getClass().getAnnotation(ApiUrl.class); 
            return ta; 

        } 
        return null; 
    } 
  //endregion 


} 

So das war der ganze Brocken ;)

Jetzt kann ich damit relativ einfach eine online API erstellen, welche auch in Android einfach zu lesen ist, da ich komplett Objektorientiert vorgehe und keine einzelnen Json Keys lesen muss, durch welche der ganze dynamische Kram wegfallen würde.

Mit diesem Code kann ich jetzt die API schön sauber mit Callback aufrufen, wie oben schon als Beispiel angemerkt.
Ich hoffe der Code hat euch nicht überfordert.
Falls doch, dann bitte Fragen stellen, das ist dann auch eine Rückmeldung an mich, was ich verbessern muss/soll und auch Feedback für ein nächstes Tutorial.

PS: Der Code der beiden Projekte ist auch hier einsehbar herunterladbar und auch zum selber herumspielen wer möchte (https://github.com/PlPt)

Noch ein kleines Wort zur App selber, sie kann nicht viel und ist ausbaufähig, bisher nur anzeigen der Datenbanken und beinhaltenden Tabellen. Wenn ich Zeit habe, werde ich das Projekt (App) eventuell weiterentwicklen, da ich nicht gerade ein Fan der PHPMyAdmin Seite im Web auf mobilen Geräten bin.
Hier war die App (Code) nur als Beispiel, damit man was einigermaßen Greifbares hat.
Also wer sich so eine App auch wünscht, dann lasst es mich wissen ;)

LG Pascal //It's not a bug, it's a feature. :) ;)

Ludy

Antworten