{"id":599,"date":"2016-03-24T19:15:05","date_gmt":"2016-03-24T17:15:05","guid":{"rendered":"https:\/\/www.bartbarnard.nl\/blog\/?p=599"},"modified":"2019-03-18T09:50:43","modified_gmt":"2019-03-18T07:50:43","slug":"het-maken-van-de-funda-harvester","status":"publish","type":"post","link":"https:\/\/www.bartbarnard.nl\/blog\/het-maken-van-de-funda-harvester\/","title":{"rendered":"Harvesting funda"},"content":{"rendered":"<p><strong>Building the Funda Harvester<\/strong><br \/>\nVoor het nieuwe database-thema van de opleiding hadden we een grote database nodig: eentje waarmee we de studenten de noodzaak van een goed datamodel konden aanleren, en de noodzaak van goed uitnormaliseren konden demonstreren. De database die de studenten zouden krijgen, zou daarom behalve behoorlijk groot ook behoorlijk onhandig in elkaar moeten zitten, zodat er veel aan te verbeteren viel.<\/p>\n<p>Op zich zou de database die we in het huidige database-thema gebruiken wel hiervoor ingezet kunnen worden. Dit betreft data van de bekende makelaarssite Funda die <del>ik jaren geleden (ik denk 2008) eens heb geharvest<\/del> een collega van me jaren geleden (ik denk 2008) eens heeft geharvest. Maar omdat ik die oorspronkelijke harverster niet zo 123 terug kon vinden (en omdat die het waarschijnlijk toch niet meer zou doen), dacht ik dat dit een mooie gelegenheid was om een nieuwe te schrijven.<\/p>\n<p>Nu schijnt er wel <a href=\"https:\/\/www.roxmedia.nl\/blog\/rox-media-ontwikkelt-api-voor-zien-en-funda-nl\/\">een API geschreven te zijn voor Funda<\/a>, maar die was niet zo heel eenvoudig te vinden. en wat ik er even over heb gelezen is deze alleen beschikbaar voor makelaars, of moet je er sowieso een API-key voor hebben. <a href=\"http:\/\/www.huizenzoeker.nl\/api\/\">Een andere site<\/a> biedt wel een redelijke API aan, maar die scheen meer gericht te zijn op ontwikkelaars die huizen op hun site willen hebben, en niet per se voor het harvesten. Nu had ik die natuurlijk wel kunnen gebruiken, bijvoorbeeld door deze via een Python-scriptje aan te roepen, maar in het kader van <em>waarom makkelijk doen als het leuker kan<\/em> besloot ik toch de boel met Java te gaan binnenhalen.<\/p>\n<p><strong>1. Parseren van de funda-site<\/strong><br \/>\nHet idee van de harvester is niet zo ingewikkeld: we halen de html van de site binnen en lopen daar doorheen op zoek naar de data die ons interesseert. Ik begon met individuele huizen, met de gedachte dat als ik \u00e9\u00e9n huis kon opslaan ik ook wel tienduizend huizen zal kunnen opslaan.<\/p>\n<p>De pagina van Funda waar de data van een specifiek huis wordt weergegeven is verrassend leesbaar. Alle gegevens staan in een div die class object-kenmerken-body heeft (figuur 1). De opzet van deze div is behoorlijk rechtdoorzee: elke waarde wordt hier weergeven als key-value-paren (listing 1)<\/p>\n<pre class=\"brush: xml; title: ; notranslate\" title=\"\">\r\n&lt;dt&gt;Aangeboden sinds&lt;\/dt&gt;\r\n&lt;dd&gt;5 weken&lt;\/dd&gt;\r\n&lt;dt&gt;Status&lt;\/dt&gt;\r\n&lt;dd&gt;Beschikbaar&lt;\/dd&gt;\r\n&lt;dt&gt;Aanvaarding&lt;\/dt&gt;\r\n&lt;dd&gt;Beschikbaar in overleg&lt;\/dd&gt;\r\n<\/pre>\n<p><em>Listing 1<\/em><\/p>\n<div id=\"attachment_613\" style=\"width: 310px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/Figuur1.png\"><img aria-describedby=\"caption-attachment-613\" loading=\"lazy\" class=\"size-medium wp-image-613\" src=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/Figuur1-300x156.png\" alt=\"De leesbaarheid van de Funda-html.\" width=\"300\" height=\"156\" srcset=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/Figuur1-300x156.png 300w, https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/Figuur1.png 990w\" sizes=\"(max-width: 300px) 100vw, 300px\" \/><\/a><p id=\"caption-attachment-613\" class=\"wp-caption-text\">De leesbaarheid van de Funda-html.<\/p><\/div>\n<p>Ik maakte een class <tt>HuisPagina<\/tt> die feitelijk een in-memory representatie van de online pagina moest gaan worden. Binnen deze class zou ik dan eenvoudig met XPath de betreffende div kunnen opzoeken, waarna ik dan binnen die context alle &lt;dt&gt;&#8217;s zou opvragen. Deze &lt;dt&gt;&#8217;s zouden als keys optreden, en de <tt>next-sibling<\/tt> hiervan als value. Dit leek een vrij eenvoudige opgave, maar het lukte me niet direct om zowel de node als de next-sibling te selecteren (nu moet ik er wel bij zeggen dat het al een tijdje terug was dat ik XPath had geschreven, dus dat was wat roestig). Uiteindelijke besloot ik dus maar een brute-force oplossing te gebruiken \u2013 namelijk door gewoon binnen de context zowel de &lt;dt&gt;&#8217;s als de &lt;dd&gt;&#8217;s op te vragen en hier overheen te itereren en een NodeList van te maken. Een eenvoudige check op de lengte van deze twee zou dan moeten aangeven of iets mis was gegaan (Listing 2):<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\nXPath xpath = XPathFactory.newInstance().newXPath();\r\nXPathExpression kenmerk = xpath.compile(&quot;\/\/div[@class='object-kenmerken-body']&quot;);\r\nNode kenmerken = (Node)kenmerk.evaluate(huis, XPathConstants.NODE);\r\n\r\nNodeList typeList = getTypeOrData(xpath, kenmerken, &quot;\/\/dt&quot;);\r\nNodeList dataList = getTypeOrData(xpath, kenmerken, &quot;\/\/dd&quot;);\r\n\t\t\r\nif (typeList.getLength() != dataList.getLength()) {\r\n\tthrow new Exception(&quot;type and data are not of the same \r\n}\r\n<\/pre>\n<p><em>Listing 2<\/em><\/p>\n<p>Die <tt>getTypeOrData(XPathContext, Node subTree, String typeOrData)<\/tt> haalde ofwel de &lt;dt&gt; ofwel de &lt;dd&gt; uit de betreffende subtree:<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\nprivate NodeList getTypeOrData(XPath context, Node subTree, String typeOrData)  {\r\n\tNodeList rv = null;\r\n\ttry {\r\n\t\tXPathExpression expr = context.compile(typeOrData);\r\n\t\trv = (NodeList) expr.evaluate(subTree, XPathConstants.NODESET);\r\n\t} catch (Exception e) {\r\n\t\te.printStackTrace();\r\n\t}\r\n\t\t\r\n\treturn rv;\r\n}\r\n<\/pre>\n<p>Helaas bleek bij uitgebreid testen dat de funda-site in sommige gevallen een lege <tt>&lt;dd&gt;<\/tt> of <tt>&lt;dt&gt;<\/tt> introduceerde \u2013 waarbij deze <em>brute force<\/em> oplossing natuurlijk niet meer werkte. Dus toch maar netjes de XPath-expressie uitzoeken om voor elke <tt>&lt;dt&gt;<\/tt> de <tt>next-sibling dd<\/tt> op te halen:<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\nString keyMatcher = &quot;\/\/dt[not(@*)]&quot;;\r\nString dataMatcher = &quot;\/\/dt[not(@*)]\/following-sibling::dd&quot;;\r\n<\/pre>\n<p>Daarmee werd deze methode gelijk een stuk robuster en transparanter. In plaats van de relatief onbekende <tt>NodeList<\/tt> kon ik nu direct een <tt>HashMap&lt;String, String&gt;<\/tt> terugsturen.<\/p>\n<p>Behalve deze data moest ik natuurlijk ook het adres en de beschrijving zien te vinden; die stonden namelijk niet in deze div. Ook hier hielp Funda weer door de pagina&#8217;s goed te structuren: het adres stond in de <tt>h1<\/tt> met <tt>class=\"object-header-title\"<\/tt>, en de postcode en woonplaats in de <tt>span class=\"object-header-subtitle\"<\/tt>.<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\nString adres = getSpecificNodeValue(&quot;\/\/h1[@class='object-header-title']&quot;);\r\nString pc_wp = getSpecificNodeValue(&quot;\/\/span[@class='object-header-subtitle']&quot;);\r\n<\/pre>\n<p>In het kader van <i>two or more, use a for<\/i>, maakte ik een methode die vanuit een opgegeven String de inhoud van die bepaalde node teruggeeft (Listing 3):<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\nprivate String getSpecificNodeValue (String node) throws Exception {\r\n\tString rv = &quot;&quot;;\r\n\tXPath xpath = XPathFactory.newInstance().newXPath();\r\n\tXPathExpression exp = xpath.compile(node);\r\n\tNode desc = (Node)exp.evaluate(huis, XPathConstants.NODE);\r\n\t\t\r\n\trv = desc.getFirstChild().getTextContent();\t\t\r\n\treturn rv;\r\n}\r\n<\/pre>\n<p><em>Listing 3<\/em><\/p>\n<p>De beschrijving van het huis stelde me nog wel voor een paar problemen. De methode <tt>getTextContent<\/tt> van <tt>javax.xml.xpath<\/tt> houdt op zo gauw er een nieuwe regel in de inhoud van de node staat, en omdat deze tekst bijna altijd over verschillende regels loopt, kreeg ik initieel alleen maar de eerste regel. De specifieke methode om alle inhoud te krijgen bleek <tt>getTextContent()<\/tt> te zijn. Deze methode geeft echter niet de html terug, die vaker wel dan niet in de beschrijving van het huis voor komt. Na enig zoeken kwam ik er achter dat er in xpath geen equivalent bestaat voor <tt>innerHTML<\/tt> (wat in JavaScript zou werken), dus besloot ik het er vooralsnog maar hier bij te laten. Wel een beetje jammer dat we de formatting van de beschrijving van het huis kwijtraken, maar dat is dan niet anders.<\/p>\n<p>Wat ik nog wel graag wilde was de mogelijkheid om de positie van het huis op een kaart te plotten \u2013 dat is in een later stadium wellicht een mooie opgave voor de studenten. Door even in de html te zoeken op google.maps kwam ik al snel achter de positie van deze gegevens (Listing 4):<\/p>\n<pre class=\"brush: xml; title: ; notranslate\" title=\"\">\r\n\r\n\r\n\r\n&lt;div class=&quot;object-map-canvas&quot; data-object-map-canvas\r\nKaart laden...\r\n&lt;script type=&quot;application\/json&quot; data-object-map-config&gt;\r\n{\r\n&quot;lat&quot;: 52.00932,\r\n&quot;lng&quot;: 4.978898,\r\n&quot;markerTitle&quot;: &quot;Dorp 192&quot;,\r\n&quot;markerUrl&quot;: &quot;\/\/assets.fstatic.nl\/855\/assets\/components\/object-map\/punaise.png&quot;\r\n}\r\n&lt;\/script&gt;\r\n\r\n&lt;noscript&gt;\r\n&lt;img alt=&quot;Dorp 192 op de kaart&quot; src=&quot;https:\/\/maps.googleapis.com\/maps\/api\/staticmap?center=52.00932,4.978898&amp;amp;amp;zoom=15&amp;amp;amp;size=500x192&amp;amp;amp;scale=2&amp;amp;amp;markers=color:0xF8B000%7C52.00932,4.978898&quot; class=&quot;object-map-static&quot;&gt;\r\n&lt;\/noscript&gt;\r\n&lt;\/div&gt;\r\n\r\n\r\n\r\n<\/pre>\n<p><em>Listing 4<\/em><\/p>\n<p>Het is mooi om te zien dat Funda een fallback heeft voor mensen die geen JavaScript in hun browser hebben (in de vorm van een statisch plaatje) en ik heb even overwogen om dit plaatje te harvesten in plaats van de latitude en longitude. Maar dat maakt de kaart minder flexibel, dus toch maar die lat en long uit het stukje JavaScript. Gelukkig was dat de enige plek waar <tt>application\/json<\/tt> als type voorkwam, dus ik kon het mooi daarop matchen \u2013 met hergebruik van de methode <tt>getSpecificNodeValue(String pattern)<\/tt>, waaruit maar weer eens het nut van abstraheren blijkt. En even een beetje rommelen met wat reguliere expressies leverde vrij snel het gewenste resultaat (Listing 5):<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\nprivate String getLatLng (String latlng) throws Exception {\r\n\tString rv = &quot;&quot;;\r\n\tString json = getSpecificNodeValue(&quot;\/\/script[@type='application\/json']&quot;);\r\n\tString newj = json.replaceAll(&quot;(\\\\r|\\\\n)&quot;, &quot; &quot;);\r\n\tnewj = newj.replaceAll(&quot;  +&quot;, &quot; &quot;);\r\n\t\t\r\n\tMatcher m = Pattern.compile(&quot;.*&quot; + latlng + &quot;[^\\\\d]+([^,]+).*&quot;,Pattern.MULTILINE).matcher(newj);\r\n\tif (m.matches()) rv = m.group(1);\r\n\t\t\t\t\r\n\treturn rv;\r\n}\r\n<\/pre>\n<p><em>Listing 5<\/em><\/p>\n<p>Alles bij elkaar was ik nu in staat om van de html van Funda een Java-object te maken dat een huis representeerde. De eigenschappen van het huis werden zo veel mogelijk in een <tt>HashMap<\/tt> opgeslagen en waar dat niet kon als separate properties van het object.<\/p>\n<p><!--nextpage--><\/p>\n<p><strong>2. Maken van een woonobject<\/strong><br \/>\nNu ik een Java-object kon maken dat een woning kon representeren, was de volgende stap uiteraard om die in de database op te slaan. Voor het betreffende thema gebruiken we in de eerste paar weken Oracle en in de laatste paar weken mysql: deze database moest dus in mysql worden opgeslagen \u2013 en, zoals gezegd, zo onhandig mogelijk. Ik besloot dus om de boel op te slaan in twee tabellen: \u00e9\u00e9n met het adres en de omschrijving van de woning en \u00e9\u00e9n met alle attributen onder elkaar, gekoppeld via een foreign key (Figuur 2).<\/p>\n<div id=\"attachment_614\" style=\"width: 169px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/Figuur2.png\"><img aria-describedby=\"caption-attachment-614\" loading=\"lazy\" class=\"size-medium wp-image-614\" src=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/Figuur2-159x300.png\" alt=\"ERD van de initi\u00eble database.\" width=\"159\" height=\"300\" srcset=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/Figuur2-159x300.png 159w, https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/Figuur2.png 191w\" sizes=\"(max-width: 159px) 100vw, 159px\" \/><\/a><p id=\"caption-attachment-614\" class=\"wp-caption-text\">ERD van de initi\u00eble database.<\/p><\/div>\n<p><!-- FIGUUR 2 --><\/p>\n<p>Ik had natuurlijk de hele boel gewoon naar mysql kunnen verplaatsen, maar omdat dit project bedoeld was om iets relatief eenvoudigs zo ingewikkeld mogelijk op te zetten leek het me voor de hand te liggen om dit in JPA te doen. Door de betreffende class van annotaties te voorzien, zorgt JPA voor de opslag in de database.<\/p>\n<p>Het was nog even gedoe om ervoor te zorgen dat de persistentie inderdaad in mysql gebeurde; de meeste voorbeeldcode die online te vinden is slaat de data native op in een objectdb. Gelukkig vond ik op github vrij snel <a href=\"https:\/\/gist.github.com\/mortezaadi\/8619433\">een link<\/a> waarin de juiste settings voor het persistente.xml bestand voor mysql in stonden weergegeven. Het was ook nog een behoorlijke puzzel om uit te vinden waar dit bestand exact moest staan, want daarover verschilden de meningen ook nog hier en daar. Uiteindelijk werkte het wanneer ik het in de directory <tt>META-INF<\/tt> in het classpath zelf zette \u2013 wat me verbaasde, want die directories heb je eigenlijk alleen maar als je met application servers te maken hebt.<\/p>\n<p>JPA werkt vrij goed. Je geeft per propertie van de class aan dat deze in de database moet komen en in welke kolom. Om bijvoorbeeld het adres, de postcode en woonplaats van het woonobject (wat allemaal Strings zijn) in de database op te slaan, gebruik je de volgende annotaties:<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\n@Column(name=&quot;address&quot;)\r\nprivate String address;\r\n\r\n@Column(name=&quot;pc_wp&quot;)\r\nprivate String pc_wp;\r\n<\/pre>\n<p><em>Listing 6<\/em><\/p>\n<p>De properties van het woonobject die ik in de tweede tabel wilde opslaan, worden in het Java-object opgeslagen in een HashMap. Omdat dit een 1:n-relatie betreft, moet hier wel een tweede tabel aan te pas komen. Hiervoor heeft JPA ook een aparte notatie (Listing 7):<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\n@ElementCollection\r\n@MapKeyColumn(name=&quot;name&quot;)\r\n@Column(name=&quot;value&quot;)\r\n@CollectionTable(name=&quot;properties&quot;, joinColumns=@JoinColumn(name=&quot;WOID&quot;))\r\nprivate Map&lt;String, String&gt; properties;\r\n<\/pre>\n<p><em>Listing 7<\/em><\/p>\n<p>Hiermee geef ik aan (regel 4) dat deze Collection in de tabel properties terecht moet komen en dat deze via de waarde in de kolom WOID gekoppeld worden aan het object waar het bijhoort (de foreign key). Ik geef verder aan (regel 2) dat de key van de HashMap in de kolom name moet worden opgeslagen, en (regel 3) de value in de kolom value. <a href=\"http:\/\/www.java2s.com\/Tutorials\/Java\/JPA\/0340__JPA_ElementCollection_CollectionTable_Override.htm\">In de documentatie<\/a> wordt \u00e9\u00e9n en ander goed beschreven.<\/p>\n<p>Er is evenwel een aantal zaken die wat lastiger zijn dan dat. Allereerst de omschrijving van het huis in kwestie. Hoewel deze, net als de postcode of de woonplaats, in een String wordt opgeslagen, kan deze evengoed behoorlijk lang zijn. Het probleem is evenwel dat een String door JPA standaard wordt gepersisteerd in een varchar(255) (terwijl <a href=\"http:\/\/stackoverflow.com\/questions\/816142\/strings-maximum-length-in-java-calling-length-method\">de maximum lengte van een String 2<sup>31<\/sup>-1 is<\/a>; dit is een mooi voorbeeld van de <a href=\"http:\/\/hibernate.org\/orm\/what-is-an-orm\/#granularity\">Problem of Granularity<\/a> in de <a href=\"https:\/\/en.wikipedia.org\/wiki\/Object-relational_impedance_mismatch\">Paradigm Mismatch<\/a>). Om dit probleem op te lossen, moest ik de omschrijving van het woonobject een aparte annotatie meegeven: <tt>@Lob<\/tt> (ik vermoed van Large Object, <a href=\"http:\/\/www.java2s.com\/Tutorials\/Java\/JPA\/0250__JPA_Lob_Column.htm\">de documentatie<\/a> is hier niet helder over):<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\n@Lob\r\n@Column(name=&quot;description&quot;)\r\nprivate String description;\r\n<\/pre>\n<p><em>Listing 8<\/em><\/p>\n<p>Een tweede punt waar ik over viel was dat er steeds maar \u00e9\u00e9n woonobject in de database terecht kwam, terwijl ik inmiddels zo ver was om de boel te testen met twee pagina&#8217;s van Funda. Initieel dacht ik dat dat kwam omdat de primary key van de objecten ofwel steeds dezelfde was ofwel werd overschreven. Een primary key geef je in JPA aan met de annotatie <tt>@Id @GeneratedValue<\/tt>. Volgens <a href=\"http:\/\/www.objectdb.com\/java\/jpa\/entity\/generated\">de documentatie<\/a> kun je hier nog wel een <em>stategy<\/em> aan meegeven, maar zou het standaard moeten werken conform de eigenschappen van de database (mysql heeft bijvoorbeeld een <em>auto_increment<\/em>). Verschillende waarden van de strategy leverden geen verbetering op: er bleef maar \u00e9\u00e9n object in de tabel verschijnen.<\/p>\n<p>Tijd om verder te kijken. Standaard geeft JPA alleen maar wat informatie over connecties en dergelijke, maar door de onderstaande regel in persistence.xml toe te voegen (Listing 9), kreeg ik de volledige sql te zien.<\/p>\n<pre class=\"brush: xml; title: ; notranslate\" title=\"\">\r\n&lt;property name=&quot;eclipselink.logging.level&quot; value=&quot;FINE&quot;\/&gt;\r\n<\/pre>\n<p><em>Listing 9<\/em><\/p>\n<p>Toen ik deze output aandachtig had bestudeerd, kwam ik er achter dat elke keer wanneer er een nieuw object werd toegevoegd, de hele database werd vervangen. Dat klopte ook, want dat stond in eveneens in persistence.xml; de instellingen die ik van github had gehaald, gingen er van uit dat je maar \u00e9\u00e9n keer de data in de database zou stoppen:<\/p>\n<pre class=\"brush: xml; title: ; notranslate\" title=\"\">\r\n&lt;property name=&quot;eclipselink.ddl-generation&quot; value=&quot;drop-and-create-tables&quot; \/&gt;\r\n<\/pre>\n<p>Uit <a href=\"http:\/\/www.eclipse.org\/eclipselink\/documentation\/2.5\/jpa\/extensions\/p_ddl_generation.htm\">de documentatie<\/a> bleek al snel dat ik de value van deze property-tag moest vervangen door <tt>create-or-extend-tables<\/tt>. Inderdaad kreeg ik nu keurig twee objecten in de database wanneer ik twee huis-pagina&#8217;s van Funda invoerde.<\/p>\n<p>Een laatste punt waar ik tegenaan liep was dat ik om de haverklap een <tt>nullpointer-exception<\/tt> kreeg. Enige onderzoek leerde dat het kwam uit de HashMap die door de huisparser wordt gevuld (Listing 10):<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\nfor (int i=0; i&lt;typeList.getLength(); i++) {\r\n    String type = typeList.item(i).getFirstChild().getNodeValue();\r\n    String data = dataList.item(i).getFirstChild().getNodeValue();\r\n \r\n    rv.put(type.trim(), data.trim());\r\n}\r\n<\/pre>\n<p><em>Listing 10<\/em><\/p>\n<p>Wanneer ik hier op regel 1 <tt>getLength()<\/tt> verving door een getal onder de 9 ging het wel goed. Het bleek dat het negende element voor de key geen waarde kon vinden in de html van Funda (omdat deze waarde een niveau dieper bleek te liggen). Ik had geen zin om de XPath nog verder uit te werken, dus besloot eenvoudigweg een check te doen op de waarde van type en data: als die Strings leeg waren of niet bestonden, zou die eigenschap van het huis niet worden opgenomen. Ik verving regel 1 dus door de onderstaande code, die een methode aanroept om de check op die Strings te doen (Listing 11):<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\n\/\/ vervanging van regel 5\r\nif (StringsAreOk(type, data)) rv.put(type.trim(), data.trim());\r\n\r\n\/\/methode om te checken of de Strings ok zijn:\r\nprivate boolean StringsAreOk(String... str) {\r\n    for (String test: str) {\r\n        if (test == null || test.equals(&quot;&quot;)) return false;\r\n    }\r\n    return true;\r\n}\r\n<\/pre>\n<p><em>Listing 11<\/em><\/p>\n<p>Ik had nu een object dat ik kon opslaan in de database. Tijd om de hele boel aan elkaar te knopen.<\/p>\n<p><!--nextpage--><\/p>\n<p><strong>Communicatie tussen deze twee lagen<\/strong><br \/>\nNu had ik een object dat een huis van Funda kon representeren, en een object dat dat kon persisteren. Het was dus zaak deze twee met elkaar in verband te brengen. Natuurlijk zou het eenvoudig geweest zijn om het object gewoon zelf in de database op te slaan, maar omdat zowel het harvesten als het opslaan de nodige tijd vereiste, leek het me beter om daar twee aparte processen van te maken, met een message queue hiertussen.<\/p>\n<p>In het onderwijs zijn al verschillende message queues ter sprake gekomen, maar <a href=\"http:\/\/www.rabbitmq.com\/\">RabbitMQ<\/a> komt altijd als \u00e9\u00e9n van de betere systemen uit de verf. Een message queue met weinig footprint en eenvoudig op te starten. Het ding draait op Erlang, maar als je dat niet hebt ge\u00efnstalleerd komt dat mee met de disk image. Eenvoudig downloaden en opstarten.<\/p>\n<p>RabbitMQ verstuurt feitelijk byte-arrays over de lijn, dus ik moest heb huis-object serialiseren. Om dit aan de ene kant te kunnen doen, en aan de andere kant van de lijn weer te kunnen deserialiseren definieerde ik een interface <tt>Perisitable<\/tt> (Listing 11):<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\npublic interface Persistable {\r\n    public byte[] getBytes();\r\n\t\r\n    public static WoonObject fromBytes(byte[] body) {\r\n        WoonObject obj = null;\r\n        \r\n        try {\r\n            ByteArrayInputStream bis = new ByteArrayInputStream(body);\r\n            ObjectInputStream ois = new ObjectInputStream(bis);\r\n            obj = (WoonObject) ois.readObject();\r\n            ois.close();\r\n            bis.close();\r\n        } catch (IOException e) {\r\n            e.printStackTrace();\r\n        } catch (ClassNotFoundException ex) {\r\n            ex.printStackTrace();\r\n        }\r\n        return obj;\r\n    }\r\n}\r\n<\/pre>\n<p><em>Listing 11<\/em><\/p>\n<p>Het is het WoonObject die deze interface implementeert, omdat dat is wat er uiteindelijk over de lijn gestuurd gaat worden. Allereerst de methode getBytes(): die serialiseert eenvoudig het huidige object naar een byte-array, die vervolgens over de lijn gestuurd kan worden (Listing 12):<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\npublic byte[] getBytes() {\r\n\tbyte[] bytes;\r\n\tByteArrayOutputStream baos = new ByteArrayOutputStream();\r\n\ttry {\r\n\t\tObjectOutputStream oos = new ObjectOutputStream(baos);\r\n\t\toos.writeObject(this);\r\n\t\toos.flush();\r\n\t\toos.reset();\r\n\t\tbytes = baos.toByteArray();\r\n\t\toos.close();\r\n\t\tbaos.close();\r\n        } catch (IOException e) {\r\n\t\te.printStackTrace();\r\n\t\tbytes = new byte[]{};\r\n\t}\r\n\t\t\r\n\treturn bytes;\r\n}\r\n<\/pre>\n<p><em>Listing 12<\/em><\/p>\n<p>Het is de class <tt>Sender<\/tt> die deze methode aanroept en het resultaat hiervan over in de queue zet (Listing 13); op deze manier kan de harvester rustig verder met de volgende pagina terwijl de Persister ervoor zorgt dat de objecten in de database terecht komen. Deze Sender maakt eerst connectie met de message queue en stuurt vervolgens het geserialiseerde WoonObject daar naartoe:<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\nchannel.basicPublish(&quot;&quot;, Settings.QUEUE_NAME, null, obj.getBytes());\r\n<\/pre>\n<p>Aan de andere kant van de lijn is een <tt>Receiver-class<\/tt> die de byte-array ontvangt. Hier maak ik gebruik van de mogelijkheid die Java8 biedt om in een interface een implementatie van een methode op te nemen. De static method <tt>fromBytes()<\/tt> hierboven (Listing 11) krijgt als parameter een byte-array en maakt hier weer een WoonObject van (dit is feitelijk een implementatie van het <a href=\"https:\/\/nl.wikipedia.org\/wiki\/Decorator\">Decorator-pattern<\/a>).<\/p>\n<p>De Receiver-class is relatief eenvoudig: deze consumeert het object in de message-queue, roept de methode <tt>fromBytes()<\/tt> aan en cast het resultaat hiervan naar een WoonObject (omdat ik alleen WoonObjecten verstuur kan dit zonder problemen), die vervolgens gepersisteerd wordt.<\/p>\n<pre class=\"brush: java; title: ; notranslate\" title=\"\">\r\nWoonObjectImpl foo = (WoonObjectImpl)Persistable.fromBytes(body);\r\nPersister.setUp();\r\nPersister.persistObject(foo);\r\nPersister.tearDown();\r\n<\/pre>\n<p><em>Listing 13<\/em><\/p>\n<p>Hieruit blijkt wel de noodzaak van dat WoonObjectImpl zowel WoonObject als Persistable implementeert: vanuit de eerste weten we dat het object gebruik maakt van JPA en de tweede zorgt ervoor dat het over de message queue gestuurd kan worden (eigenlijk moet die interface dus hernoemd worden naar Transferable). Als het object dat is binnengekomen is gedeserialiseerd, kan ik eenvoudig de JPA methodes daarbinnen aanroepen om het in mysql op te slaan. Het totale plaatje ziet er uiteindelijk als volgt uit (Figuur 3):<\/p>\n<div id=\"attachment_629\" style=\"width: 310px\" class=\"wp-caption aligncenter\"><a href=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/architectuur.jpg\"><img aria-describedby=\"caption-attachment-629\" loading=\"lazy\" class=\"size-medium wp-image-629\" src=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/architectuur-300x224.jpg\" alt=\"Uiteindelijke architectuur.\" width=\"300\" height=\"224\" srcset=\"https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/architectuur-300x224.jpg 300w, https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/architectuur-1024x765.jpg 1024w, https:\/\/www.bartbarnard.nl\/blog\/wp-content\/uploads\/2016\/03\/architectuur.jpg 1296w\" sizes=\"(max-width: 300px) 100vw, 300px\" \/><\/a><p id=\"caption-attachment-629\" class=\"wp-caption-text\">Uiteindelijke architectuur.<\/p><\/div>\n<p>Op deze manier kon ik een heel stuk van de Funda-database harvesten, zonder dat \u00e9\u00e9n van beide kanten last had van falende performance. Het is misschien een aardige extra opgave voor de eerstejaars studenten om deze code te analyseren en te verbeteren, maar vooralsnog doet het systeem prima waar het voor gemaakt is.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Building the Funda Harvester Voor het nieuwe database-thema van de opleiding hadden we een grote database nodig: eentje waarmee we de studenten de noodzaak van een goed datamodel konden aanleren, en de noodzaak van goed uitnormaliseren konden demonstreren. De database die de studenten zouden krijgen, zou daarom behalve behoorlijk groot ook behoorlijk onhandig in elkaar<\/p>\n<p class=\"more-link\"><a href=\"https:\/\/www.bartbarnard.nl\/blog\/het-maken-van-de-funda-harvester\/\" class=\"themebutton2\">Read More<\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[19],"tags":[],"_links":{"self":[{"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/posts\/599"}],"collection":[{"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/comments?post=599"}],"version-history":[{"count":33,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/posts\/599\/revisions"}],"predecessor-version":[{"id":990,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/posts\/599\/revisions\/990"}],"wp:attachment":[{"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/media?parent=599"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/categories?post=599"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.bartbarnard.nl\/blog\/wp-json\/wp\/v2\/tags?post=599"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}