{"id":12493,"date":"2017-05-26T09:27:59","date_gmt":"2017-05-26T07:27:59","guid":{"rendered":"http:\/\/www.devapp.it\/wordpress\/?p=12493"},"modified":"2017-05-26T09:27:59","modified_gmt":"2017-05-26T07:27:59","slug":"creare-api-rest-con-spark-java-gson-e-rethinkdb","status":"publish","type":"post","link":"https:\/\/www.devapp.it\/wordpress\/creare-api-rest-con-spark-java-gson-e-rethinkdb\/","title":{"rendered":"Creare API REST con Spark Java, Gson e RethinkDB"},"content":{"rendered":"<p>Al giorno d&#8217;oggi esistono molti servizi moderni, affidabili e reattivi, basati su programmi di piccole dimensioni e dalla configurazione agevole. Ne abbiamo gi\u00e0 conosciuti alcuni <a href=\"http:\/\/www.devapp.it\/wordpress\/creare-api-rest-velocemente-con-spark-java\/\">su questo sito<\/a> come Spark Java che permette di <strong>creare velocemente API REST<\/strong> e <a href=\"http:\/\/www.devapp.it\/wordpress\/rethinkdb-il-database-json-per-il-web-realtime\/\" target=\"_blank\" rel=\"noopener noreferrer\">RethinkDB<\/a> che offre database JSON ideali per funzionalit\u00e0 real-time. In questo articolo, li metteremo insieme facendoci aiutare anche da <a href=\"https:\/\/github.com\/google\/gson\" target=\"_blank\" rel=\"noopener noreferrer\">Gson<\/a>, altra efficacissima libreria per il formato JSON offerta da Google: realizzeremo un&#8217;applicazione di esempio il cui codice potr\u00e0 essere scaricato <a href=\"http:\/\/www.devapp.it\/wordpress\/wp-content\/uploads\/2016\/10\/EsempioSparkRethinkDB.zip\" target=\"_blank\" rel=\"noopener noreferrer\">qui<\/a>.<\/p>\n<h2>Cosa vogliamo realizzare?<\/h2>\n<p>Il nostro scopo \u00e8 quello di gestire un database RethinkDB tramite il suo\u00a0<a href=\"https:\/\/rethinkdb.com\/blog\/official-java-driver\/\" target=\"_blank\" rel=\"noopener noreferrer\">driver per Java<\/a>. Per il resto, Spark Java ci offrir\u00e0 gli strumenti per l&#8217;interfacciamento in rete e tutto il codice di gestione dei dati sar\u00e0 racchiuso nella nostra classe <em>LocalDataService<\/em>. Il database sar\u00e0 composto da una sola tabella la cui finalit\u00e0 sar\u00e0 conservare due stringhe &#8211; <em>text1<\/em> e <em>text2<\/em> &#8211; ad uso molto generico (potrebbe trattarsi di una nota con il suo contenuto o altro simile) nonch\u00e8 un id univoco in formato stringa assegnato da RethinkDB.<\/p>\n<p>La prima cosa che ci serve \u00e8 <strong>risolvere le dipendenze<\/strong>. Il nostro progetto avr\u00e0 bisogno delle tre librerie che abbiamo nominato per cui potremo scaricare i relativi file jar dai siti ufficiali o ricorrere ad un sistema di gestione come Maven. Noi, ad esempio, abbiamo percorso quest&#8217;ultima via creando\u00a0un progetto Maven per Eclipse nel cui\u00a0file pom.xml abbiamo inserito le seguenti dipendenze:<\/p>\n<pre class=\"lang:xhtml decode:true\">&lt;dependencies&gt;\r\n    &lt;dependency&gt;\r\n      &lt;groupId&gt;com.google.code.gson &lt;\/groupId&gt;\r\n      &lt;artifactId&gt;gson&lt;\/artifactId&gt;\r\n      &lt;version&gt;2.7&lt;\/version&gt;\r\n    &lt;\/dependency&gt;\r\n    &lt;dependency&gt;\r\n      &lt;groupId&gt;com.sparkjava&lt;\/groupId&gt;\r\n      &lt;artifactId&gt;spark-core&lt;\/artifactId&gt;\r\n      &lt;version&gt;2.5&lt;\/version&gt;\r\n    &lt;\/dependency&gt;\r\n    &lt;dependency&gt;\r\n      &lt;groupId&gt;com.rethinkdb&lt;\/groupId&gt;\r\n      &lt;artifactId&gt;rethinkdb-driver&lt;\/artifactId&gt;\r\n      &lt;version&gt;2.3.3&lt;\/version&gt;\r\n    &lt;\/dependency&gt;\r\n&lt;\/dependencies&gt;<\/pre>\n<h2>Un database con RethinkDB<\/h2>\n<p>Per eseguire l&#8217;applicazione, \u00e8 necessario<a href=\"https:\/\/rethinkdb.com\/docs\/install\/\" target=\"_blank\" rel=\"noopener noreferrer\"> installare RethinkDB<\/a>. Il procedimento \u00e8 rapidissimo ed una volta lanciato il server si pu\u00f2 invocare da browser l&#8217;indirizzo <em>http:\/\/localhost:8080\/<\/em> per accedere al suo pannello di amministrazione. Gestiremo il rapporto con questa sorgente dati tramite la classe <em>Connector<\/em>:<\/p>\n<pre class=\"lang:java decode:true\">public class Connector {\r\n\tprivate String dbname=\"test\";\r\n\tprivate String hostname=\"localhost\";\r\n\tprivate int port=28015;\r\n\tprivate String tableName;\r\n\tprivate Connection conn=null;\r\n\tprivate RethinkDB r =null;\r\n\t\r\n\tpublic Connector(String db, String tableName)\r\n\t{\r\n\t\tdbname=db;\r\n\t\tthis.tableName=tableName;\r\n\t\tr= RethinkDB.r;\r\n\t\ttry{\r\n\t    \tr.dbCreate(dbname).run(getConnection());\r\n\t    \tr.db(dbname).tableCreate(tableName).run(getConnection());\r\n\t    \t}\r\n\t    catch(ReqlOpFailedError e){}\r\n\t}\r\n\t\r\n\tpublic Connection getConnection()\r\n\t{\r\n\t  if (conn==null || !conn.isOpen())\r\n\t   conn = r.connection().hostname(hostname).port(port).connect();\r\n\t  return conn;\r\n\t}\r\n\t\r\n\tpublic Table getTable()\r\n\t{\r\n\t\treturn r.db(dbname).table(tableName);\r\n\t}\r\n\t\r\n}<\/pre>\n<p>Tutta la gestione dei dati verr\u00e0 effettuata tramite l&#8217;oggetto<em> r<\/em>, di classe RethinkDB, che rappresenta la nostra porta d&#8217;accesso alla sorgente.<\/p>\n<p>In questa classe:<\/p>\n<ul>\n<li>il metodo <em>getConnection<\/em> aprir\u00e0 una connessione con RethinkDB, via rete, sfruttando hostname e porta TCP che abbiamo definito nei membri privati;<\/li>\n<li>all&#8217;interno del costruttore creiamo\u00a0il database con la sua unica tabella: potrebbero gi\u00e0 esistere entrambi e ci\u00f2 genererebbe un&#8217;eccezione da cui ci tuteliamo con un blocco try&#8230;catch;<\/li>\n<li><em>getTable<\/em> offrir\u00e0 un collegamento alla\u00a0tabella.<\/li>\n<\/ul>\n<p>Il Connector serve prevalentemente a stabilire connessione al database e verr\u00e0 utilizzata esclusivamente dalla classe <em>LocalDataService<\/em>. All&#8217;interno di quest&#8217;ultima ne definiamo un&#8217;altra, <em>DataNode<\/em>, che rappresenta il singolo record della tabella (per la precisione,\u00a0dovremmo chiamarlo &#8220;documento&#8221; in quanto stiamo\u00a0parlando di una tecnologia NoSQL):<\/p>\n<pre class=\"lang:java decode:true\">public class LocalDataService {\r\n\t\r\n\tpublic static class DataNode\r\n\t{\r\n\t\tString id;\r\n\t\tString text1;\r\n\t\tString text2;\r\n\t\t\r\n\t\tDataNode(String t1, String t2)\r\n\t\t{\r\n\t\t\ttext1=t1;\r\n\t\t\ttext2=t2;\r\n\t\t}\r\n\t\t\r\n\t\tDataNode(String id, String t1, String t2)\r\n\t\t{\r\n\t\t\tthis(t1,t2);\r\n\t\t\tthis.id=id;\r\n\t\t}\r\n\t\t\r\n\t\tMapObject toMapObject()\r\n\t\t{\r\n\t\t\tMapObject res=new MapObject();\r\n\t\t\tif (text1!=null)\r\n\t\t\t\tres.put(\"text1\", text1);\r\n\t\t\tif (text2!=null)\r\n\t\t\t\tres.put(\"text2\", text2);\r\n\t\t\treturn res;\r\n\t\t}\r\n\t\t\r\n\t}\r\n\r\n     \/*\r\n      *\r\n      *  metodi di accesso ai dati della classe\r\n      *\r\n      *\/\r\n}<\/pre>\n<p>vi abbiamo inserito anche il metodo <em>toMapObject<\/em> che esporta i dati in un tipo di struttura dati a mappa utilizzata da RethinkDB.<\/p>\n<pre class=\"lang:java decode:true \">public class LocalDataService {\r\n\t\r\n       \/*\r\n        *\r\n        * definizione della classe DataNode\r\n        *\r\n        *\/\r\n\r\n\tprivate Connector conn;\r\n\tprivate boolean valid;\r\n\t\r\n\tpublic boolean isValid() {\r\n\t\treturn valid;\r\n\t}\r\n\r\n\tpublic LocalDataService(String db, String tableName)\r\n\t{\r\n\t\ttry{\r\n\t\tconn=new Connector(db, tableName);\r\n\t\tvalid=true;\r\n\t\t}\r\n\t\tcatch(ReqlDriverError rde)\r\n\t\t{\r\n\t\t\tvalid=false;\r\n\t\t}\r\n\t}\r\n\t\r\n\tpublic HashMap&lt;String, String&gt; save(DataNode dn)\r\n\t{\r\n\t\tHashMap&lt;String, Object&gt; result=conn.getTable().insert(dn.toMapObject()).run(conn.getConnection());\r\n\t\tif ((Long)result.get(\"inserted\")==1)\r\n\t\t{\r\n\t\t\tArrayList&lt;String&gt; l=(ArrayList&lt;String&gt;) result.get(\"generated_keys\");\r\n\t\t\tif (l.size()&gt;0)\r\n\t\t\t{\r\n\t\t\t\tHashMap&lt;String, String&gt; res=new HashMap&lt;String, String&gt;();\r\n\t\t\t\tres.put(\"key\", l.get(0));\r\n\t\t\t    return res;\r\n\t\t\t}\r\n\t\t}\r\n\t\tthrow new RuntimeException();\r\n\t}\r\n\t\r\n\tpublic HashMap&lt;String, Integer&gt; tableSize()\r\n\t{\r\n\t\tCursor cursor=conn.getTable().run(conn.getConnection());\r\n\t\tint size=cursor.toList().size();\r\n\t\tHashMap&lt;String, Integer&gt; res=new HashMap&lt;String, Integer&gt;();\r\n\t\tres.put(\"size\", size);\r\n\t    return res;\r\n\t}\r\n\t\r\n\tpublic Object tableElement(String key)\r\n\t{\r\n\t\tObject o=conn.getTable().get(key).run(conn.getConnection());\r\n\t\tif (o==null)\r\n\t\t\tthrow new NotValidKeyException(key);\r\n\t    return o;\r\n\t}\r\n\t\r\n\tpublic Object deleteElement(String key)\r\n\t{\r\n\t\tHashMap&lt;String,Long&gt; o=conn.getTable().get(key).delete().run(conn.getConnection());\r\n\t\tHashMap&lt;String,String&gt; res=new HashMap&lt;String,String&gt;();\r\n\t\tif (o.get(\"deleted\")==1)\r\n\t\t{\r\n\t\t\tres.put(\"key\", key);\r\n\t\t\tres.put(\"deleted\", \"true\");\r\n\t\t\treturn res;\r\n\t\t}\r\n\t\telse if (o.get(\"deleted\")==0 &amp;&amp; o.get(\"skipped\")==1)\r\n\t\t\tthrow new NotValidKeyException(key);\r\n\t\tthrow new InternalServerException(\"Internal Server Exception\");\r\n\t}\r\n\t\r\n\tpublic HashMap&lt;String,String&gt; updateElement(String key, DataNode dn)\r\n\t{\r\n\t\tHashMap&lt;String,Long&gt; o=conn.getTable().get(key).update(dn.toMapObject()).run(conn.getConnection());\r\n\t\tHashMap&lt;String,String&gt; res=new HashMap&lt;String,String&gt;();\r\n\t\tif (o.get(\"replaced\")==1)\r\n\t\t{\r\n\t\t\tres.put(\"key\", key);\r\n\t\t\tres.put(\"updated\", \"true\");\r\n\t\t\treturn res;\r\n\t\t}\r\n\t\telse if (o.get(\"skipped\")==1)\r\n\t\t\tthrow new NotValidKeyException(key);\r\n\t\telse\r\n\t\t\tif (o.get(\"unchanged\")==1)\r\n\t\t\t{\r\n\t\t\t\tres.put(\"key\", key);\r\n\t\t\t\tres.put(\"updated\", \"false\");\r\n\t\t\t\treturn res;\r\n\t\t\t}\r\n\t\tthrow new InternalServerException(\"Internal Server Exception\");\r\n\t}\r\n}<\/pre>\n<p>Il <em>costruttore della classe<\/em> inizializza un oggetto Connector il cui fallimento renderebbe impossibile l&#8217;utilizzo delle API: questo pu\u00f2, ad esempio, succedere se non abbiamo avviato RethinkDB o se quest&#8217;ultimo ha subito un crash. Il membro booleano <em>valid<\/em> serve proprio al codice che sfrutta Spark Java per sapere se il DataService \u00e8 attivo: in caso negativo, come vedremo, le API verranno arrestate all&#8217;avvio.<\/p>\n<p>Gli altri metodi sono quelli che vengono invocati dalle API REST e sono\u00a0esemplificativi dei vari possibili casi: lettura, scrittura, cancellazione e modifica. Ognuno di essi, in base all&#8217;esito dell&#8217;elaborazione, dovr\u00e0 produrre un risultato del tipo richiesto che poi tradurremo in JSON ed invieremo al client o un&#8217;opportuna eccezione, utilizzata anch&#8217;essa per rispondere alla richiesta. <strong>Richiederemo sempre un riferimento alla tabella tramite il Connector e poi eseguiremo un apposito metodo di ricerca o modifica<\/strong>. Queste invocazioni termineranno sempre con la chiamata a <em>run<\/em> che vuole un riferimento alla connessione valida, fornito dal metodo <em>getConnection<\/em> del Connector. Vediamoli pi\u00f9 in dettaglio:<\/p>\n<ul>\n<li><em>tableSize<\/em> e <em>tableElement<\/em> sono i due metodi di lettura: il primo restituisce un numero intero indicante quanti sono i record al momento trattati dal database mentre il secondo restituisce una HashMap che descrive il documento recuperato in base alla chiave. Si noti che entrambi richiedono un riferimento alla tabella dal Connector;<\/li>\n<li><em>deleteElement<\/em> cerca un documento contenente la chiave fornita e, se rinvenuto, ne ordina la cancellazione. La risposta di RethinkDB sar\u00e0 un oggetto JSON in cui la propriet\u00e0 deleted impostata a 1 indicher\u00e0 cancellazione avvenuta mentre skipped a 1 significher\u00e0 che la chiave non \u00e8 stata trovata;<\/li>\n<li><em>save<\/em> riceve un oggetto DataNode e lo inserisce nella tabella tramite metodo <em>insert<\/em>. E&#8217; a questo punto che utilizziamo il metodo di conversione <em>toMapObject<\/em> di DataNode. Importante in questo caso recuperare la chiave appena generata che restituiremo al client che potrebbe farne uso per aggiornare i suoi dati \u00a0o l&#8217;interfaccia utente se ne possiede una;<\/li>\n<li>updateElement non \u00e8 molto differente dai precedenti: grazie al driver di RethinkDB cerchiamo per chiave il documento da modificare e gli forniamo un oggetto DataNode (anche parzialmente compilato) per vedere cosa c&#8217;\u00e8 da modificare. In questo caso, \u00e8 pi\u00f9 articolato valutare l&#8217;esito dell&#8217;operazione. Ci sono tre flag attivati nell&#8217;oggetto JSON risultato e li valutiamo tutti: replaced vale 1 se siamo riusciti ad apportare modifiche, unchanged in caso contrario e skipped se non si \u00e8 riuscito proprio a svolgere l&#8217;operazione.<\/li>\n<\/ul>\n<p>Le <strong>eccezioni<\/strong> richiamate in questo codice sono state definite da noi nel package <em>exceptions<\/em> e ci\u00f2 serve solo a comprendere meglio i risultati delle interrogazioni sul database.<\/p>\n<h2>Finalmente le API REST<\/h2>\n<p>Come si utilizzano le API REST con Spark Java \u00e8 stato gi\u00e0 oggetto del nostro <a href=\"http:\/\/www.devapp.it\/wordpress\/creare-api-rest-velocemente-con-spark-java\/\">precedente articolo<\/a>, come ricordato in precedenza. E&#8217; necessario invocare un metodo (il cui nome coincide con\u00a0quello del verbo HTTP cui ci riferiamo) e fornire pattern per l&#8217;URL che risponder\u00e0, il codice di reazione da attivare ed eventualmente &#8211; noi lo facciamo in questo caso &#8211; un <strong>ResponseTransformer<\/strong> che formatta la risposta prima di restituirla al client: all&#8217;interno\u00a0useremo Gson per convertire i dati in JSON:<\/p>\n<pre class=\"lang:java decode:true \">public class JsonTransformer implements ResponseTransformer {\r\n\r\n\tprivate Gson gson;\r\n\r\n    public JsonTransformer(Gson gson) {\r\n\t\tthis.gson=gson;\r\n\t}\r\n    @Override\r\n    public String render(Object model) {\r\n        return gson.toJson(model);\r\n    }\r\n    \r\n    public String renderError(String message) {\r\n        HashMap&lt;String, String&gt;  error=new HashMap&lt;String, String&gt;();\r\n    \terror.put(\"error\", message);\r\n    \treturn render(error);\r\n    }\r\n\r\n}<\/pre>\n<p>Ai fini della <strong>trasformazione in JSON<\/strong> delle risposte al client, Spark Java invocher\u00e0 sempre il metodo <em>render<\/em> in automatico di cui abbiamo fatto <em>override<\/em> mentre l&#8217;altro, <em>renderError<\/em>, lo usiamo noi esplicitamente per formattare i messaggi di errore.<\/p>\n<p>Il codice \u00e8 suddiviso in due porzioni: prima definiamo i gestori di eccezioni per ognuno dei casi di errore che abbiamo riscontrato nel LocalDataService e poi attiviamo tutti metodi HTTP che ci interessano:<\/p>\n<pre class=\"lang:java decode:true\">public class App {\r\n    public static void main( String[] args) {\r\n    \tLocalDataService ds=new LocalDataService(\"elenco\", \"dati\");\r\n    \t\r\n    \tif (!ds.isValid())\r\n    \t{\r\n    \t\tSystem.out.println(\"Error: Connection to backed service failed\");\r\n    \t\tSystem.out.println(\"Server stopped\");\r\n    \t\tSystem.exit(-1);\r\n    \t}\r\n    \tSystem.out.println(\"Server running...\");\r\n    \t\r\n    \tGson gson=new Gson();\r\n    \tJsonTransformer transformer=new JsonTransformer(gson);\r\n    \t\r\n    \tString BASE_URL=\"\/textbase\/v1\";\r\n    \tport(1121);\r\n\r\n\/\/ gestione eccezioni\r\n    \t\r\n    \texception(InternalServerException.class, (exception, request, response) -&gt; {\r\n    \t\tresponse.status(501);\r\n\t\t\tresponse.body(transformer.renderError(exception.getMessage()));\r\n    \t});\r\n    \t\r\n    \texception(ResourceNotFoundException.class, (exception, request, response) -&gt; {\r\n    \t\tresponse.status(404);\r\n\t\t\tresponse.body(transformer.renderError(exception.getMessage()));\r\n    \t});\r\n    \t\r\n    \texception(NotValidKeyException.class, (exception, request, response) -&gt; {\r\n    \t\tresponse.status(400);\r\n\t\t\tresponse.body(transformer.renderError(exception.getMessage()));\r\n    \t});\r\n    \t\r\n    \texception(NotValidDataException.class, (exception, request, response) -&gt; {\r\n    \t\tresponse.status(400);\r\n\t\t\tresponse.body(transformer.renderError(exception.getMessage()));\r\n    \t});\r\n\r\n\r\n\/\/ chiamate REST\r\n    \t\r\n    \tget(BASE_URL+\"\/data\/count\", \"application\/json\", (request, response) -&gt; {\r\n    \t    return ds.tableSize();\r\n    \t}, transformer);\r\n    \t\r\n    \t\r\n    \tget(BASE_URL+\"\/data\/:key\", \"application\/json\", (req, res) -&gt; {\r\n    \t\tString key=req.params(\":key\");\r\n            return ds.tableElement(key);\r\n        }, transformer);\r\n    \t\r\n    \tpost(BASE_URL+\"\/data\/add\", \"application\/json\", (request, response) -&gt; {\r\n    \t\tDataNode newItem=gson.fromJson(request.body(), DataNode.class);\r\n    \t\tresponse.status(201);\r\n    \t    return ds.save(newItem);\r\n    \t}, transformer);\r\n    \t\r\n    \tdelete(BASE_URL+\"\/data\/:key\", \"application\/json\", (req, res) -&gt; {\r\n    \t\tString key=req.params(\":key\");\r\n    \t\treturn ds.deleteElement(key);\r\n        }, transformer);\r\n    \t\r\n    \tput(BASE_URL+\"\/data\/:key\", \"application\/json\", (req, res) -&gt; {\r\n    \t\tString key=req.params(\":key\");\r\n    \t\tif (req.body().length()==0)\r\n    \t\t\tthrow new NotValidDataException(\"No data sent in request body\");\r\n    \t\tDataNode dataToBeUpdated=gson.fromJson(req.body(), DataNode.class);\r\n    \t\treturn ds.updateElement(key, dataToBeUpdated);\r\n        }, transformer);\r\n    \t\r\n    \t\r\n    }\r\n}<\/pre>\n<h2>Conclusioni<\/h2>\n<p>Con questo esempio, abbiamo voluto dimostrare quanto servizi moderni &#8211; non tanto come epoca di nascita ma come &#8220;mentalit\u00e0&#8221; &#8211; possano rendere agevole la realizzazione di API al fine di offrire un backend server alle nostre app mobile. In un post gi\u00e0 apparso su questo sito, abbiamo parlato dell&#8217;attuale tendenza alla realizzazione di applicativi\u00a0server basati su <a href=\"http:\/\/www.devapp.it\/wordpress\/microservizi-per-logiche-applicative-di-grandi-dimensioni\/\" target=\"_blank\" rel=\"noopener noreferrer\">microservizi<\/a> piuttosto che su architetture monolitiche: Spark Java \u00e8 uno strumento ideale a contesti simili e se ne dimostra continuamente all&#8217;altezza.<\/p>\n<p>Continueremo ad aggiornarvi su queste interessanti tematiche: non mancate di seguirci e farci sapere le vostre opinioni.<\/p>\n<p>Alla prossima!<\/p>\n<p>&nbsp;<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Al giorno d&#8217;oggi esistono molti servizi moderni, affidabili e reattivi, basati su programmi di piccole dimensioni e&#8230;<\/p>\n","protected":false},"author":561,"featured_media":13049,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"footnotes":""},"categories":[1],"tags":[1530,1526,1713,1779,1780,1756,1778],"class_list":["post-12493","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-tutorial-pratici","tag-database","tag-java","tag-microservices","tag-nosql-database","tag-rest-api","tag-rethinkdb","tag-spark-java"],"acf":[],"aioseo_notices":[],"_links":{"self":[{"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/posts\/12493","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/users\/561"}],"replies":[{"embeddable":true,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/comments?post=12493"}],"version-history":[{"count":10,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/posts\/12493\/revisions"}],"predecessor-version":[{"id":13050,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/posts\/12493\/revisions\/13050"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/media\/13049"}],"wp:attachment":[{"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/media?parent=12493"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/categories?post=12493"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.devapp.it\/wordpress\/wp-json\/wp\/v2\/tags?post=12493"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}