diff --git a/src/nl/surf/eduhub_rio_mapper/commands/link.clj b/src/nl/surf/eduhub_rio_mapper/commands/link.clj index dfb4079b..b759237b 100644 --- a/src/nl/surf/eduhub_rio_mapper/commands/link.clj +++ b/src/nl/surf/eduhub_rio_mapper/commands/link.clj @@ -128,7 +128,7 @@ (case type "education-specification" ["aanleveren_opleidingseenheid" "eigenOpleidingseenheidSleutel"] ("course" "program") ["aanleveren_aangebodenOpleiding" "eigenAangebodenOpleidingSleutel"]) - rio-obj (rio.loader/rio-finder getter rio-config request)] + rio-obj (rio.loader/rio-finder getter request)] (if (nil? rio-obj) (throw (ex-info "404 Not Found" {:phase :resolving})) (let [rio-obj (xmlclj->duo-hiccup rio-obj) diff --git a/src/nl/surf/eduhub_rio_mapper/commands/processing.clj b/src/nl/surf/eduhub_rio_mapper/commands/processing.clj index 704b226f..1341e51a 100644 --- a/src/nl/surf/eduhub_rio_mapper/commands/processing.clj +++ b/src/nl/surf/eduhub_rio_mapper/commands/processing.clj @@ -185,8 +185,8 @@ output (if (nil? ooapi-summary) diff (assoc diff :opleidingseenheidcode rio-code))] (merge output (dry-run-status rio-summary ooapi-summary)))) -(defn- course-program-dry-run-handler [ooapi-entity {::ooapi/keys [id] :keys [institution-oin] :as request} {:keys [rio-config ooapi-loader]}] - (let [rio-obj (rio.loader/find-aangebodenopleiding id institution-oin rio-config) +(defn- course-program-dry-run-handler [ooapi-entity {::ooapi/keys [id] :keys [institution-oin] :as request} {:keys [getter ooapi-loader]}] + (let [rio-obj (rio.loader/find-aangebodenopleiding id getter institution-oin) rio-summary (dry-run/summarize-aangebodenopleiding-xml rio-obj) offering-summary (->> (ooapi.loader/load-offerings ooapi-loader request) (map dry-run/summarize-offering) diff --git a/src/nl/surf/eduhub_rio_mapper/rio/loader.clj b/src/nl/surf/eduhub_rio_mapper/rio/loader.clj index 91e276ef..dfc2f360 100644 --- a/src/nl/surf/eduhub_rio_mapper/rio/loader.clj +++ b/src/nl/surf/eduhub_rio_mapper/rio/loader.clj @@ -33,11 +33,11 @@ [nl.surf.eduhub-rio-mapper.utils.xml-validator :as xml-validator]) (:import (org.w3c.dom Element))) -(def aangeboden-opleiding "aangebodenOpleiding") -(def aangeboden-opleidingen-van-organisatie "aangebodenOpleidingenVanOrganisatie") -(def opleidingseenheid "opleidingseenheid") -(def opleidingseenheden-van-organisatie "opleidingseenhedenVanOrganisatie") -(def opleidingsrelaties-bij-opleidingseenheid "opleidingsrelatiesBijOpleidingseenheid") +(def aangeboden-opleiding-type "aangebodenOpleiding") +(def aangeboden-opleidingen-van-organisatie-type "aangebodenOpleidingenVanOrganisatie") +(def opleidingseenheid-type "opleidingseenheid") +(def opleidingseenheden-van-organisatie-type "opleidingseenhedenVanOrganisatie") +(def opleidingsrelaties-bij-opleidingseenheid-type "opleidingsrelatiesBijOpleidingseenheid") (def opleidingseenheid-namen #{:hoOpleiding :particuliereOpleiding :hoOnderwijseenhedencluster :hoOnderwijseenheid}) @@ -46,56 +46,59 @@ #{:aangebodenHOOpleidingsonderdeel :aangebodenHOOpleiding :aangebodenParticuliereOpleiding}) ;; NOTE: aangeboden opleidingen are referenced by OOAPI UID -(def aangeboden-opleiding-types #{aangeboden-opleiding - aangeboden-opleidingen-van-organisatie}) +(def aangeboden-opleiding-types #{aangeboden-opleiding-type + aangeboden-opleidingen-van-organisatie-type}) (def valid-get-types (into aangeboden-opleiding-types - #{opleidingseenheid - opleidingseenheden-van-organisatie - opleidingsrelaties-bij-opleidingseenheid})) + #{opleidingseenheid-type + opleidingseenheden-van-organisatie-type + opleidingsrelaties-bij-opleidingseenheid-type})) (def schema "http://duo.nl/schema/DUO_RIO_Raadplegen_OnderwijsOrganisatie_V4") (def contract "http://duo.nl/contract/DUO_RIO_Raadplegen_OnderwijsOrganisatie_V4") (def validator (xml-validator/create-validation-fn "DUO_RIO_Raadplegen_OnderwijsOrganisatie_V4.xsd")) -(defn- single-xml-unwrapper - "Find the content of the first child of `element` with type `tag`. - - Returns `nil` if no matching element is there" - [element tag] - (some-> element - (xml-utils/get-in-dom [tag]) - (.getFirstChild) - (.getTextContent))) +;; De externe identificatie komt niet voor in RIO +;; Handled separately because this is an expected outcome, and handling it is part of the normal program flow. +(def missing-entity "A01161") (defn goedgekeurd? [^Element element] {:pre [element]} - (= "true" (single-xml-unwrapper element "ns2:requestGoedgekeurd"))) + (= "true" (xml-utils/single-xml-unwrapper element "ns2:requestGoedgekeurd"))) (defn log-rio-action-response [msg element] (logging/with-mdc - {:identificatiecodeBedrijfsdocument (single-xml-unwrapper element "ns2:identificatiecodeBedrijfsdocument")} + {:identificatiecodeBedrijfsdocument (xml-utils/single-xml-unwrapper element "ns2:identificatiecodeBedrijfsdocument")} (log/debugf (format "RIO %s; SUCCESS: %s" msg (goedgekeurd? element))))) -;; De externe identificatie komt niet voor in RIO -;; Handled separately because this is an expected outcome, and handling it is part of the normal program flow. -(def missing-entity "A01161") +(defn- handle-resolver-success [element] + ;; TODO: this is ugly, but we don't know at this stage what entity we tried to resolve. + (let [code (or (xml-utils/single-xml-unwrapper element "ns2:opleidingseenheidcode") + (xml-utils/single-xml-unwrapper element "ns2:aangebodenOpleidingCode"))] + (log-rio-action-response (str "SUCCESSFUL RESOLVE:" code) element) + code)) + +(defn- handle-resolver-error [element] + (let [foutmelding (-> element + xml-utils/element->edn + :opvragen_rioIdentificatiecode_response + :foutmelding) + id (-> foutmelding + :sleutelgegeven + :sleutelwaarde) + foutcode (:foutcode foutmelding) + error-msg (if (= missing-entity foutcode) + (str "Object with id (" id ") not found in RIO via resolve") + (str "Resolve of object " id " failed with error code " foutcode))] + (log-rio-action-response error-msg element) + (when-not (= missing-entity foutcode) + (throw (ex-info error-msg {:retryable? false}))))) (defn- rio-resolver-response [^Element element] {:pre [element]} (if (goedgekeurd? element) - ;; TODO: this is ugly, but we don't know at this stage what entity we tried to resolve. - (let [code (or (single-xml-unwrapper element "ns2:opleidingseenheidcode") - (single-xml-unwrapper element "ns2:aangebodenOpleidingCode"))] - (log-rio-action-response (str "SUCCESSFUL RESOLVE:" code) element) - code) - (let [foutmelding (-> element xml-utils/element->edn :opvragen_rioIdentificatiecode_response :foutmelding) - id (-> foutmelding :sleutelgegeven :sleutelwaarde)] - (when-not (= missing-entity (:foutcode foutmelding)) - (log-rio-action-response (str "Resolve of object " id " failed with error code " (:foutcode foutmelding)) element) - (throw (ex-info (str "Resolve of object " id " failed with error code " (:foutcode foutmelding)) {:retryable? false}))) - (log-rio-action-response (str "Object with id (" id ") not found in RIO via resolve") element) - nil))) + (handle-resolver-success element) + (handle-resolver-error element))) (defn- rio-relation-getter-response [^Element element] {:post [(s/valid? (s/nilable ::relations/relation-vector) %)]} @@ -115,14 +118,6 @@ :valid-to (:opleidingsrelatieEinddatum m) :opleidingseenheidcodes #{(:opleidingseenheidcode samenhang) (:opleidingseenheidcode m)}}))))))) -(defn- rio-xml-getter-response [^Element element] - (assert (goedgekeurd? element)) ; should fail elsewhere with error http code otherwise - (-> element xml-utils/dom->str)) - -(defn- rio-json-getter-response [^Element element] - (assert (goedgekeurd? element)) ; should fail elsewhere with error http code otherwise - (-> element xml-utils/element->edn json/write-str)) - (defn make-datamap [sender-oin recipient-oin] {:schema schema @@ -140,15 +135,11 @@ {:type type, :body body}))) body) -(defn- handle-opvragen-request [type response-handler request] - (let [tag (str "ns2:opvragen_" type "_response")] - (-> request - http-utils/send-http-request - (guard-getter-response type tag) - xml-utils/str->dom - .getDocumentElement - (xml-utils/get-in-dom ["SOAP-ENV:Body" tag]) - response-handler))) +(defn- extract-body-element [response tag] + (-> response + xml-utils/str->dom + .getDocumentElement + (xml-utils/get-in-dom ["SOAP-ENV:Body" tag]))) (defn make-resolver "Return a RIO resolver. @@ -170,73 +161,58 @@ ("course" "program") :duo:eigenAangebodenOpleidingSleutel) id]] (make-datamap institution-oin recipient-oin) - credentials)] - (handle-opvragen-request "rioIdentificatiecode" - rio-resolver-response - (assoc credentials - :url read-url - :method :post - :body xml - :headers {"SOAPAction" (str contract "/opvragen_rioIdentificatiecode")} - :connection-timeout connection-timeout-millis - :content-type :xml))))))) + credentials) + request {:url read-url + :method :post + :body xml + :headers {"SOAPAction" (str contract "/opvragen_rioIdentificatiecode")} + :connection-timeout connection-timeout-millis + :content-type :xml} + tag "ns2:opvragen_rioIdentificatiecode_response"] + (-> (http-utils/send-http-request (merge credentials request)) + (guard-getter-response "rioIdentificatiecode" tag) + (extract-body-element tag) + rio-resolver-response)))))) (defn- valid-onderwijsbestuurcode? [code] (re-matches #"\d\d\dB\d\d\d" code)) -(defn- response-handler-for-type [response-type type] - (case response-type - :literal identity - :xml rio-xml-getter-response - :json rio-json-getter-response - ;; If unspecified, use edn for relations and json for everything else - (if (= type opleidingsrelaties-bij-opleidingseenheid) - rio-relation-getter-response - rio-json-getter-response))) - -(defn find-opleidingseenheid [rio-code getter institution-oin] - {:pre [rio-code]} - (-> (getter {::rio/type opleidingseenheid - ::rio/opleidingscode rio-code - :institution-oin institution-oin - :response-type :xml}) +(defn find-named-element [response-body name-set] + (-> response-body clj-xml/parse-str xml-seq - (xml-utils/find-in-xmlseq #(when (opleidingseenheid-namen (:tag %)) %)))) - -(def opvragen-aangeboden-opleiding-soap-action (str "opvragen_" aangeboden-opleiding)) -(def opvragen-aangeboden-opleiding-response-tagname (str "ns2:" opvragen-aangeboden-opleiding-soap-action "_response")) - -(defn find-aangebodenopleiding - "Returns aangeboden opleiding as parsed xml document. Returns nil if not found. - - Requires institution-oin and recipient-oin (which should be distinct)." - [aangeboden-opleiding-code - institution-oin - {:keys [read-url credentials recipient-oin] :as _config}] - {:pre [aangeboden-opleiding-code institution-oin recipient-oin (not= institution-oin recipient-oin)]} - (let [soap-req (soap/prepare-soap-call opvragen-aangeboden-opleiding-soap-action - [[:duo:aangebodenOpleidingCode aangeboden-opleiding-code]] - (make-datamap institution-oin - recipient-oin) - credentials) - request (assoc credentials - :url read-url - :method :post - :body soap-req - :headers {"SOAPAction" (str contract "/" opvragen-aangeboden-opleiding-soap-action)} - :content-type :xml)] - (-> request - http-utils/send-http-request - (guard-getter-response type opvragen-aangeboden-opleiding-response-tagname) - clj-xml/parse-str - xml-seq - (xml-utils/find-in-xmlseq #(and (aangeboden-opleiding-namen (:tag %)) %))))) - -(defn rio-finder [getter rio-config {::ooapi/keys [type] ::rio/keys [opleidingscode aangeboden-opleiding-code] :keys [institution-oin] :as _request}] + (xml-utils/find-in-xmlseq #(when (name-set (:tag %)) + %)))) + +(defn find-rio-object [rio-code getter institution-oin type] + {:pre [rio-code]} + (let [[code-name name-set] (if (= type opleidingseenheid-type) + [::rio/opleidingscode opleidingseenheid-namen] + [::rio/aangeboden-opleiding-code aangeboden-opleiding-namen])] + (-> (getter {::rio/type type + code-name rio-code + :institution-oin institution-oin + :response-type :literal}) + (find-named-element name-set)))) + +(defn find-opleidingseenheid [rio-code getter institution-oin] + (find-rio-object rio-code getter institution-oin opleidingseenheid-type)) + +(defn find-aangebodenopleiding [rio-code getter institution-oin] + (find-rio-object rio-code getter institution-oin aangeboden-opleiding-type)) + +(defn rio-finder [getter {::ooapi/keys [type] ::rio/keys [opleidingscode aangeboden-opleiding-code] :keys [institution-oin] :as _request}] (case type - "education-specification" (find-opleidingseenheid opleidingscode getter institution-oin) - ("course" "program") (find-aangebodenopleiding aangeboden-opleiding-code institution-oin rio-config))) + "education-specification" (find-rio-object opleidingscode getter institution-oin opleidingseenheid-type) + ("course" "program") (find-rio-object aangeboden-opleiding-code getter institution-oin aangeboden-opleiding-type))) + +(defn- rio-xml-getter-response [^Element element] + (assert (goedgekeurd? element)) ; should fail elsewhere with error http code otherwise + (-> element xml-utils/dom->str)) + +(defn- rio-json-getter-response [^Element element] + (assert (goedgekeurd? element)) ; should fail elsewhere with error http code otherwise + (-> element xml-utils/element->edn json/write-str)) (defn make-getter "Return a function that looks up an 'aangeboden opleiding' by id. @@ -246,55 +222,69 @@ [{:keys [read-url credentials recipient-oin]}] {:pre [read-url]} (fn getter [{::ooapi/keys [id] - ::rio/keys [type opleidingscode] + ::rio/keys [type opleidingscode aangeboden-opleiding-code] :keys [institution-oin pagina response-type] :or {pagina 0}}] - {:pre [(or (and (aangeboden-opleiding-types type) id) - opleidingscode)]} + {:pre [(or (aangeboden-opleiding-types type) + opleidingscode) + (or (not= type aangeboden-opleidingen-van-organisatie-type) + id) + (or (not= type aangeboden-opleiding-type) + aangeboden-opleiding-code)]} (when-not (valid-get-types type) (throw (ex-info (str "Unexpected type: " type) {:id id :opleidingscode opleidingscode :retryable? false}))) - (when (and (= type opleidingseenheden-van-organisatie) + (when (and (= type opleidingseenheden-van-organisatie-type) (not (valid-onderwijsbestuurcode? opleidingscode))) (throw (ex-info (str "Type 'onderwijsbestuurcode' has ID invalid format: " opleidingscode) {:type type :retryable? false}))) - (let [soap-action (str "opvragen_" type) - rio-sexp (condp = type - ;; Command line only. - opleidingseenheden-van-organisatie - [[:duo:onderwijsbestuurcode opleidingscode] ;; FIXME: this is not an opleidingscode! - [:duo:pagina pagina]] - - ;; Command line only. - aangeboden-opleidingen-van-organisatie - [[:duo:onderwijsaanbiedercode id] - [:duo:pagina pagina]] - - opleidingsrelaties-bij-opleidingseenheid - [[:duo:opleidingseenheidcode opleidingscode]] - - aangeboden-opleiding - [[:duo:aangebodenOpleidingCode id]] - - opleidingseenheid - [[:duo:opleidingseenheidcode opleidingscode]])] - (logging/with-mdc {:soap-action soap-action} - (let [xml (soap/prepare-soap-call soap-action - rio-sexp - (make-datamap institution-oin recipient-oin) - credentials)] - (handle-opvragen-request type - (fn [element] - (log-rio-action-response type element) - ((response-handler-for-type response-type type) element)) - (assoc credentials - :url read-url - :method :post - :body xml - :headers {"SOAPAction" (str contract "/" soap-action)} - :content-type :xml))))))) + (logging/with-mdc {:soap-action (str "opvragen_" type)} + (let [soap-action (str "opvragen_" type) + rio-sexp (condp = type + ;; Command line only. + opleidingseenheden-van-organisatie-type + [[:duo:onderwijsbestuurcode opleidingscode] ;; FIXME: this is not an opleidingscode! + [:duo:pagina pagina]] + + ;; Command line only. + aangeboden-opleidingen-van-organisatie-type + [[:duo:onderwijsaanbiedercode id] + [:duo:pagina pagina]] + + opleidingsrelaties-bij-opleidingseenheid-type + [[:duo:opleidingseenheidcode opleidingscode]] + + aangeboden-opleiding-type + [[:duo:aangebodenOpleidingCode aangeboden-opleiding-code]] + + opleidingseenheid-type + [[:duo:opleidingseenheidcode opleidingscode]]) + xml (soap/prepare-soap-call soap-action + rio-sexp + (make-datamap institution-oin recipient-oin) + credentials) + request {:url read-url + :method :post + :body xml + :headers {"SOAPAction" (str contract "/" soap-action)} + :content-type :xml} + tag (str "ns2:opvragen_" type "_response") + response-body (-> (http-utils/send-http-request (merge credentials request)) + (guard-getter-response type tag)) + body-element (extract-body-element response-body tag) + + response-handler (case response-type + :literal identity + :xml rio-xml-getter-response + :json rio-json-getter-response + ;; If unspecified, use edn for relations and json for everything else + (if (= type opleidingsrelaties-bij-opleidingseenheid-type) + rio-relation-getter-response + rio-json-getter-response))] + (log-rio-action-response type body-element) + (response-handler (if (= :literal response-type) response-body body-element)))))) diff --git a/src/nl/surf/eduhub_rio_mapper/utils/xml_utils.clj b/src/nl/surf/eduhub_rio_mapper/utils/xml_utils.clj index c16b2f5c..3b9a511d 100644 --- a/src/nl/surf/eduhub_rio_mapper/utils/xml_utils.clj +++ b/src/nl/surf/eduhub_rio_mapper/utils/xml_utils.clj @@ -164,3 +164,13 @@ :or {initial-indent "" indent-str " "}}] (debug-print-xml-node root initial-indent indent-str)) + +(defn single-xml-unwrapper + "Find the content of the first child of `element` with type `tag`. + + Returns `nil` if no matching element is there" + [element tag] + (some-> element + (get-in-dom [tag]) + (.getFirstChild) + (.getTextContent))) diff --git a/test/nl/surf/eduhub_rio_mapper/e2e_helper.clj b/test/nl/surf/eduhub_rio_mapper/e2e_helper.clj index e5ce8411..51c277b1 100644 --- a/test/nl/surf/eduhub_rio_mapper/e2e_helper.clj +++ b/test/nl/surf/eduhub_rio_mapper/e2e_helper.clj @@ -33,7 +33,8 @@ [nl.surf.eduhub-rio-mapper.specs.ooapi :as ooapi] [nl.surf.eduhub-rio-mapper.utils.http-utils :as http-utils] [nl.surf.eduhub-rio-mapper.utils.xml-utils :as xml-utils]) - (:import (java.util Base64) + (:import [java.net ConnectException] + (java.util Base64) (java.io ByteArrayInputStream StringWriter) (javax.xml.xpath XPathFactory))) @@ -449,7 +450,7 @@ "Call RIO `opvragen_opleidingsrelatiesBijOpleidingseenheid`." [code] (print-boxed "rio-relations" - (rio-get {::rio-helper/type rio-loader/opleidingsrelaties-bij-opleidingseenheid + (rio-get {::rio-helper/type rio-loader/opleidingsrelaties-bij-opleidingseenheid-type ::rio-helper/opleidingscode code :institution-oin (:institution-oin @client-info)}))) @@ -475,7 +476,7 @@ "Call RIO `opvragen_opleidingseenheid`." [code] (print-boxed "rio-opleidingseenheid" - (-> {::rio-helper/type rio-loader/opleidingseenheid + (-> {::rio-helper/type rio-loader/opleidingseenheid-type ::rio-helper/opleidingscode code :institution-oin (:institution-oin @client-info) :response-type :literal} @@ -485,7 +486,7 @@ "Call RIO `opvragen_aangebodenOpleiding`." [id] (print-boxed "rio-aangebodenopleiding" - (-> {::rio-helper/type rio-loader/aangeboden-opleiding + (-> {::rio-helper/type rio-loader/aangeboden-opleiding-type ::ooapi/id id :institution-oin (:institution-oin @client-info) :response-type :literal} @@ -547,7 +548,7 @@ (http/get (str @base-url "/metrics") {:throw-exceptions false}) true - (catch java.net.ConnectException _ + (catch ConnectException _ false))] (when-not result (Thread/sleep wait-for-serve-api-sleep-msec) diff --git a/test/nl/surf/eduhub_rio_mapper/interaction_test.clj b/test/nl/surf/eduhub_rio_mapper/interaction_test.clj index dfe3501a..0b12040d 100644 --- a/test/nl/surf/eduhub_rio_mapper/interaction_test.clj +++ b/test/nl/surf/eduhub_rio_mapper/interaction_test.clj @@ -178,12 +178,16 @@ (let [vcr (helper/make-vcr :playback) config (config/make-config) client-info (clients-info/client-info (:clients config) "rio-mapper-dev.jomco.nl") - rio-config (:rio-config config)] + rio-config (:rio-config config) + handlers (processing/make-handlers {:rio-config rio-config + :gateway-root-url (:gateway-root-url config) + :gateway-credentials (:gateway-credentials config)}) + getter (:getter handlers)] (testing "found aangeboden opleiding" (binding [http-utils/*vcr* (vcr "test/fixtures/aangeboden-finder-test" 1 "finder")] - (let [result (rio.loader/find-aangebodenopleiding "bd6cb46b-3f4e-49c2-a1f7-e24ae82b0672" (:institution-oin client-info) rio-config)] + (let [result (rio.loader/find-aangebodenopleiding "bd6cb46b-3f4e-49c2-a1f7-e24ae82b0672" getter (:institution-oin client-info))] (is (some? result))))) (testing "did not find aangeboden opleiding" (binding [http-utils/*vcr* (vcr "test/fixtures/aangeboden-finder-test" 2 "finder")] - (let [result (rio.loader/find-aangebodenopleiding "bbbbbbbb-3f4e-49c2-a1f7-e24ae82b0673" (:institution-oin client-info) rio-config)] + (let [result (rio.loader/find-aangebodenopleiding "bbbbbbbb-3f4e-49c2-a1f7-e24ae82b0673" getter (:institution-oin client-info))] (is (nil? result)))))))