Clojure update maps in a list - clojure

I'm trying add some modifications to this script. This method is given:
(defn- emit-class!
[[class fields]]
(let [vals {:view? (:view? class)
:type (if (:view? class) "View" "Object")
:package (:package class)
:name (str (:dollar-name class) Icepick/SUFFIX)
:target (:dotted-name class)
:parent (if-let [parent (:qualified-parent-name class)]
(str parent Icepick/SUFFIX)
(if (:view? class) "View" "Object"))
:fields fields}
file-name (str (:package class) "." (:dollar-name class) Icepick/SUFFIX)
file-object (file-object file-name (:element class))]
(doto (.openWriter file-object)
(.write (mustache/render-string template vals))
(.flush)
(.close))))
As far as I understand this code fields is a list containing maps. If I print the content with
(doseq [fff fields
[k v] fff]
(info (str k " " fff)))
Then I get this content
{
:name "counterAlt",
:enclosing-class
{
:package "com.some.package",
:dotted-name "DemoPresenter",
:dollar-name "DemoPresenter",
:annote (#object[com.sun.tools.javac.code.Attribute$Compound 0x6054b6e "#com.Bla"]),
:elem #object[com.sun.tools.javac.code.Symbol$ClassSymbol 0x21312e84 "com.evernote.android.common.demo.DemoPresenter"],
:view? false,
:qualified-parent-name nil
},
:bundler false,
:method "Int"
}
What I'm trying to do is to add another value called fieldsCapitalize to the vals variable, where the maps in the list are exactly the same, but only the name is capitalized. In this sample counterAlt should become CounterAlt.
I have a working capitalize function, but I'm unable to create another list with the updated maps. What's the best way to achieve this in this function?

I finally found a way, not sure if it's best approach though
(defn capitalize [s]
(if (> (count s) 0)
(str (Character/toUpperCase (.charAt s 0))
(subs s 1))
s))
(defn myfunc [m] (assoc m :name (capitalize (get m :name))))
(defn- emit-class!
[[class fields]]
(let [vals {:view? (:view? class)
:type (if (:view? class) "View" "Object")
:package (:package class)
:name (str (:dollar-name class) Icepick/SUFFIX)
:target (:dotted-name class)
:parent (if-let [parent (:qualified-parent-name class)]
(str parent Icepick/SUFFIX)
(if (:view? class) "View" "Object"))
:fields fields
:cap (map myfunc fields)}
file-name (str (:package class) "." (:dollar-name class) Icepick/SUFFIX)
file-object (file-object file-name (:element class))]
(doto (.openWriter file-object)
(.write (mustache/render-string template vals))
(.flush)
(.close))))

Related

How to create an array of custom java object class in Clojure?

This is my code. I would like to make an array of Fruit (an custom java object class).
(defn build-fruit
[part]
(let [name (:name part)
color (:color part)
price (:price part)
is_available (:is_available part)]
(-> (Fruit/newBuilder)
(.setName name)
(.setColor color)
(.setPrice price)
(.setIsAvailable is_available)
(.build))))
(defn build-fruits
[result length]
(loop [remaining-fruits result
final-fruits (make-array Fruit length)]
(if (empty? remaining-fruits)
final-fruits
(let [[part & remaining] remaining-fruits]
(recur remaining
(into-array final-fruits (build-fruit part)))))) ;got an error here
)
(defn -getAllFruits [this ^Empty req ^StreamObserver res]
(let [result (clj-grpc.database/findAllFruit)
length (count result)
fruits (build-fruits result length)]
(doto res
(.onNext (-> (ListOfFruits/newBuilder)
(.addAllFruit fruits)
(.build)))
(.onCompleted))))
But got an error
java.lang.ClassCastException: [Lorg.example.fruitproto.Fruit; cannot be cast to java.lang.Class
at clojure.core$into_array.invokeStatic(core.clj:3431)
at clojure.core$into_array.invoke(core.clj:3431)
at clj_grpc.fruitservice$build_fruits.invokeStatic(fruitservice.clj:118)
at clj_grpc.fruitservice$build_fruits.invoke(fruitservice.clj:110)
My guess is I cann't use make-array. I also can't use simple array like this [] because it also got another error. What is the proper way to achieve this?
Here is a standalone example of using into-array, building an array of LocalDate rather than Fruit:
(ns overflow.example
(:import (java.time LocalDate)))
(defn build-local-date [{:keys [year month day]}]
(LocalDate/of year month day))
(defn build-local-dates [xs]
(into-array LocalDate (map build-local-date xs)))

change all keywords in collection, removing dots from the namespace and replacing with a dash

How can I map a function over a vector of maps (which also contain vectors of maps) to remove all dots from keyword namespaces?
So, given:
[{:my.dotted/namespace "FOO"}
{:my.nested/vec [{:another.dotted/namespace "BAR"
:and.another/one "FIZ"}]}]
becomes:
[{:my-dotted/namespace "FOO"}
{:my-nested/vec [{:another-dotted/namespace "BAR"
:and-another/one "FIZ"}]}]
Sounds like a job for clojure.walk!
You can traverse the entire data structure and apply a transforming function (transform-map in my version) to all the sub-forms that switches a map's keys (here, with dotted->dashed) when it encounters one.
(require '[clojure
[walk :as walk]
[string :as str]])
(defn remove-dots-from-keys
[data]
(let [dotted->dashed #(-> % str (str/replace "." "-") (subs 1) keyword)
transform-map (fn [form]
(if (map? form)
(reduce-kv (fn [acc k v] (assoc acc (dotted->dashed k) v)) {} form)
form))]
(walk/postwalk transform-map data)))
I'm partial to clojure.walk for these sort of jobs. The basic idea is to create functions that perform the replacement you want if given a value that should be replaced, else returns the argument. Then you hand that function and a structure to postwalk (or prewalk) and it walks the data structure for you, replacing each value with the return value of the function on it.
(ns replace-keywords
(:require [clojure.walk :refer [postwalk]]
[clojure.string :refer [join]]))
(defn dash-keyword [k]
(when (keyword? k)
(->> k
str
(map (some-fn {\. \-} identity))
rest
join
keyword)))
(dash-keyword :foo.bar/baz)
;; => :foo-bar/baz
(defonce nested [ {:my-dotted/namespace "FOO"}
{:my-nested/vec [ {:another-dotted/namespace "BAR"
:and-another/one "FIZ"} ]}])
(postwalk (some-fn dash-keyword identity) nested)
;; =>[{:my-dotted/namespace "FOO"}
;; {:my-nested/vec [{:another-dotted/namespace "BAR",
;; :and-another/one "FIZ"}]}]
Twice here I use the combination of some-fn with a function that returns a replacement or nil, which can be a nice way to combine several "replacement rules" - if none of the earlier ones fire then identity will be the first to return a non-nil value and the argument won't be changed.
This problem can also be solved without clojure.walk:
(require '[clojure.string :as str])
(defn dot->dash [maps]
(mapv #(into {} (for [[k v] %]
[(keyword (str/replace (namespace k) "." "-") (name k))
(if (vector? v) (dot->dash v) v)]))
maps))
Example:
(dot->dash [{:my.dotted/namespace "FOO"}
{:my.nested/vec [{:another.dotted/namespace "BAR"
:and.another/one "FIZ"}]}])
;=> [{:my-dotted/namespace "FOO"}
; {:my-nested/vec [{:another-dotted/namespace "BAR"
; :and-another/one "FIZ"}]}]

How to parse xml and get an vector for some attributes on an element

I know how to extract one attribute using zip-xml/attr, but how to extract multiple attributes?
e.g I have the following
<table>
<column name="col1" type="varchar" length="8"/>
<column name="col2" type="varchar" length="16"/>
<column name="col3" type="int" length="16"/>
<table>
And the expected result is. A silly way is to call zip-xml/attr for each attribute, but is there any elegant way to do that?
[["co11" "varchar" 8] [["co12" "varchar" 16] [["co13" "int" 16]
My advice is to use a tree-walking function to extract the interesting data from the XML tree. clojure.walk has several of these, but here I use tree-seq from core clojure to just produce a seq of nodes and work on that. This function takes two functions - a branch? predicate which checks if a node can have children and a children function which gets them. I use :content for both, as tags with no nested tags produce nil, which is a falsey value and so it works also as a predicate.
(->> (clojure.xml/parse "res/doc.xml") ;;source file for your xml
(tree-seq :content :content) ;; Produce a seq by walking the tree
(filter #(= :column (:tag %))) ;;Take only :column tags
(mapv (comp vec vals :attrs)))
;;Collect the values of the :attrs maps into vectors
;;and collect those into a vector with mapv
Your desired output had unmatched square brackets, but I assume it should be like
[["col1" "varchar" "8"] ["col2" "varchar" "16"] ["col3" "int" "16"]]
which was my return value. However, this is potentially brittle - you're relying on the maps returned by clojure.xml/parse preserving the ordering of the attributes in the XML in order to know what the data means. That's not really part of the contract of maps. As an implementation detail it creates clojure.lang.PersistentStructMaps which apparently do have this feature, but it might not always be so.
Alternatively you could use just (mapv :attrs) to keep the whole of the map in there.
The right solution depends on how large and complex the XML is and to some extent, what you know about its structure. If it needs to be very generic, then you need to have quite a lot of logic to navigate the nodes etc. However, if it is a known format and you know what nodes you are interested in, its pretty straight-forward.
I used clojure.zip to create a zipper from the XML file and then use clojure.data.zip.xml to extract the nodes/paths I was interested in. I then defined simple helper functions to process specific nodes. This was pretty much my first bit of clojure and I've not yet gone back to it to re-factor it and refine/clarify some of my very rough clojure idioms based on what I've learnt since, but in the spirit of an example being worth 1000 words, here it is -
(ns arcis.models.nessus
(:use [taoensso.timbre :only [trace debug info warn error fatal]])
(:require [arcis.util :as util]
[arcis.models.db :as db]
[clojure.java.io :as io]
[clojure.xml :as xml]
[clojure.zip :as zip]
[clojure.data.zip.xml :as zx]))
(def nessus-host-keys [:hostname :host_fqdn
:system_type :operating_system
:operating_system_unsupported])
(def used-nessus-host-keys (conj nessus-host-keys
:host_start :host_end
:items :traceroute_hop_0 :traceroute_hop_1
:traceroute_hop_2 :traceroute_hop_3
:traceroute_hop_4 :traceroute_hop_5
:traceroute_hop_6 :traceroute_hop_7
:traceroute_hop_8 :traceroute_hop_9
:traceroute_hop_10 :traceroute_hop_11
:traceroute_hop_12 :traceroute_hop_13
:traceroute_hop_14 :traceroute_hop_15
:traceroute_hop_16 :traceroute_hop_17
:host_ip :patch_summary_total_cves
:cpe_0 :cpe_1 :cpe_2 :cpe_3 :cpe_4 :cpe_5
:cpe_6 :cpe_7 :cpe_8 :cpe_9))
(def nessus-item-keys [:port :svc_name :protocol :severity :plugin_id
:plugin_output])
(def used-nessus-item-keys (conj nessus-item-keys
:plugin_details
:plugin_name
:plugin_family))
(def nessus-plugin-keys [:plugin_id :plugin_name :plugin_family :fname
:script_version :plugin_type :exploitability_ease
:vuln_publication_date :cvss_temporal_data
:solution :cvss_temporal_score :risk_factor
:description :cvss_vector :synopsis
:patch_publication_date :exploit_available
:plugin_publication_date :plugin_modification_date
:cve :bid :exploit_framework_canvas :edb_id
:exploit_framework_metasploit :exploit_framework_core
:metasploit_name :canvas_package :osvdb :cwe
:cvss_temporal_vector :cvss_base_score :cpe
:exploited_by_malware])
(def used-nessus-plugin-keys (conj nessus-plugin-keys
:xref :see_also :cert
:attachment :iava :stig_severity :hp
:secunia :iawb :msft))
(def show-unprocessed true)
(defn log-unprocessed [title vls]
(if (and show-unprocessed
(seq vls))
(println (str "Unprocessed " title ": " vls))))
;;; parse nessus report
(defn parse-xref [xref]
{:xref (first (:content xref))})
(defn parse-see-also [see-also]
{:see_also (first (:content see-also))})
(defn parse-plugin [plugin]
{(util/db-keyword (name (:tag plugin))) (first (:content plugin))})
(defn parse-contents [cont]
(let [xref (mapv parse-xref (filter #(= (:tag %) :xref) cont))
see-also (mapv parse-see-also (filter #(= (:tag %) :see-also) cont))
details (reduce merge {}
(map parse-plugin
(remove #(or (= (:tag %) :xref)
(= (:tag %) :see-also)) cont)))]
(assoc details
:see_also see-also
:xref xref)))
(defn fix-item-keywords [item]
(let [ks (keys item)]
(into {}
(for [k ks]
[(util/db-keyword (name k))
(k item)]))))
(defn parse-item [item]
(let [attrs (fix-item-keywords (:attrs item))
contents (parse-contents (:content item))]
(assoc attrs
:plugin_output (:plugin_output contents)
:plugin_details (assoc (dissoc contents :plugin_output)
:plugin_id (:plugin_id attrs)
:plugin_family (:plugin_family attrs)))))
(defn parse-properties [props]
(into {}
(for [p props]
[(util/db-keyword (:name (:attrs p)))
(first (:content p))])))
(defn parse-host [h]
(let [items (map first (zx/xml-> h :ReportItem))
properties (:content (first (zx/xml1-> h :HostProperties)))]
(assoc (parse-properties properties)
:hostname (zx/attr h :name)
:items (mapv parse-item items))))
(defn parse-hosts [hosts]
(mapv parse-host hosts))
(defn parse-file [f]
(let [root (zip/xml-zip (xml/parse (io/file f)))
report-xml (zx/xml1-> root :Report)
hosts (zx/xml-> report-xml :ReportHost)]
{:report_name (zx/attr report-xml :name)
:policy (zx/text (zx/xml1-> root :Policy :policyName))
:hosts (parse-hosts hosts)}))
;;; insert nessus records into db
(defn mk-host-rec [scan-id host]
(let [[id err] (db/get-sequence-nextval "host_seq")]
(if (nil? err)
(assoc (util/build-map host nessus-host-keys)
:ipv4 (:host_ip host)
:scan_start (util/from-nessus-date (:scan_start host))
:scan_end (util/from-nessus-date (:scan_end host))
:total_cves (:patch_summary_total_cves host)
:id id
:scan_id scan-id)
nil)))
(defn insert-patches [p]
(when (seq p)
(db/insert-nessus-host-patch (first p))
(recur (rest p))))
(defn insert-host-patch [id host]
(let [p-keys (filter #(re-find #"patch_summary_*" %) (map name (keys host)))
recs (map (fn [s]
{:id (first (db/get-sequence-nextval "patch_seq"))
:host_id id
:summary ((keyword (str "patch_summary_txt_" s)) host)
:cve_num ((keyword (str "patch_summary_cve_num_" s)) host)
:cves ((keyword (str "patch_summary_cves_" s)) host)})
(filter seq
(map #(second (re-find #"patch_summary_txt_(.*)" %))
p-keys)))]
(insert-patches recs)
(util/remove-keys host (map keyword p-keys))))
(defn mk-item-rec [host-id item]
(let [[id err] (db/get-sequence-nextval "item_seq")]
(assoc (util/build-map item nessus-item-keys)
:host_id host-id
:id id)))
(defn insert-item [host-id item]
(let [rec (mk-item-rec host-id item)
not-done (keys (util/remove-keys item used-nessus-item-keys))]
(log-unprocessed "Item Keys" not-done)
(db/insert-nessus-report-item rec)
(:plugin_id item)))
(defn mk-plugin-rec [item]
(let [rec (util/build-map (:plugin_details item) nessus-plugin-keys)
not-used (keys (util/remove-keys (:plugin_details item)
used-nessus-plugin-keys))]
(log-unprocessed "Plugin Keys" not-used)
(assoc rec
:vuln_publication_date (util/from-nessus-date
(:vuln_publication_date rec))
:patch_publication_date (util/from-nessus-date
(:patch_publication_date rec))
:plugin_publication_date (util/from-nessus-date
(:plugin_publication_date rec))
:plugin_modification_date (util/from-nessus-date
(:plugin_modificaiton_date rec)))))
(defn insert-xref [plugin-id xrefs]
(when (seq xrefs)
(let [xref {:id (first (db/get-sequence-nextval "xref_seq"))
:plugin_id plugin-id
:xref (:xref (first xrefs))}]
(db/insert-nessus-xref xref)
(recur plugin-id (rest xrefs)))))
(defn insert-see-also [plugin-id see-also]
(when (seq see-also)
(let [sa {:id (first (db/get-sequence-nextval "ref_seq"))
:plugin_id plugin-id
:reference (:see_also (first see-also))}]
(db/insert-nessus-ref sa)
(recur plugin-id (rest see-also)))))
(defn insert-plugin [item]
(let [rec (mk-plugin-rec item)
xref (:xref (:plugin_details item))
see-also (:see_also (:plugin_details item))]
(if (seq xref)
(insert-xref (:plugin_id rec) xref))
(if (seq see-also)
(insert-see-also (:plugin_id rec) see-also))
(db/upsert-nessus-plugin rec)))
(defn insert-items [host-id items plugin-set]
(if (empty? items)
plugin-set
(let [p (insert-item host-id (first items))]
(if-not (contains? plugin-set p)
(insert-plugin (first items)))
(recur host-id (rest items) (conj plugin-set p)))))
(defn insert-host [scan-id host plugin-set]
(if-let [h-rec (mk-host-rec scan-id host)]
(let [[v err] (db/insert-nessus-host h-rec)
items (:items host)]
(if (nil? err)
(let [host2 (insert-host-patch (:id h-rec) host)]
(log-unprocessed "Host Keys" (keys (util/remove-keys
host2 used-nessus-host-keys)))
(insert-items (:id h-rec) items plugin-set))
plugin-set))
plugin-set))
(defn insert-hosts
([id hosts]
(insert-hosts id hosts #{}))
([id hosts plugins]
(if (empty? hosts)
plugins
(let [plugin-set (insert-host id (first hosts) plugins)]
(recur id (rest hosts) plugin-set)))))
(defn mk-scan-record [id report]
{:id id
:name (:report_name report)
:scan_dt (util/to-sql-date)
:policy (:policy report)
:entered_dt (util/to-sql-date)})
(defn store-report [update-plugins report]
(let [[id err] (db/get-sequence-nextval "nscan_seq")
scan-rec (mk-scan-record id report)]
(if (nil? err)
(let [[v e] (db/insert-nessus-scan scan-rec)]
(if (nil? e)
(if update-plugins
(let [plugin-list (set (first (db/select-nessus-plugin-ids)))]
[(insert-hosts id (:hosts report) plugin-list) nil])
[(insert-hosts id (:hosts report)) nil])
[v e]))
[id err])))
(defn process-nessus-report [update-plugins filename]
(let [report (parse-file filename)]
(println (str "Report: " (:report_name report)
"\nPolicy: " (:policy report)
"\nHost Records: " (count (:hosts report))))
(store-report update-plugins report)))
Magos's answer using tree-seq is perfectly fine, but there's no reason to abandon zippers; filtering using zippers is more succinct and the arguably the "clojure" way. (note this example uses data.xml ([org.clojure/data.xml "0.0.8"]) instead of clojure.xml).
(require '[clojure.data.zip.xml :as zf])
(require '[clojure.zip :as z])
(def ex
"<table>
<column name=\"col1\" type=\"varchar\" length=\"8\"/>
<column name=\"col2\" type=\"varchar\" length=\"16\"/>
<column name=\"col3\" type=\"int\" length=\"16\"/>
</table>")
(let [x (z/xml-zip (clojure.data.xml/parse-str ex))]
(->> (zf/xml-> x :column) ;;equivalent to (->> treeseq ... filter)
flatten
(keep :attrs)
(map vals)))
;>>> (("col1" "varchar" "8") ("col2" "varchar" "16") ("col3" "int" "16"))
But the xml-> macro simply applies functions in order, so you can do the following:
(let [x (z/xml-zip (clojure.data.xml/parse-str ex))]
(->> (zf/xml-> x :column #(keep :attrs %))
(map vals)))
;>>> (("col1" "varchar" "8") ("col2" "varchar" "16") ("col3" "int" "16"))

Append to an attribute in Enlive

Is it possible to append a value to an attribute using enlive?
example: I have this
edit
and would like this
edit
I am currently doing this:
(html/defsnippet foo "views/foo.html" [:#main]
[ctxt]
[:a] (html/set-attr :href (str "/item/edit/" (ctxt :id))))
But I would prefer not to embed the URL into my code, by just appending the id to the existing URL
(html/defsnippet foo "views/foo.html" [:#main]
[ctxt]
[:a#href] (html/append (ctxt :id)))
#ddk answer is spot on but you may prefer a more generic way to solve the problem
(defn update-attr [attr f & args]
(fn [node]
(apply update-in node [:attrs attr] f args))))
and then
(update-attr :href str "123")
You could always write your own append-attr in the same vein as set-attr. Here is my attempt
(defn append-attr
[& kvs]
(fn [node]
(let [in-map (apply array-map kvs)
old-attrs (:attrs node {})
new-attrs (into {} (for [[k v] old-attrs]
[k (str v (get in-map k))]))]
(assoc node :attrs new-attrs))))
Which gives the following, when appending "/bar" to href, on enlive's representation of A link
((append-attr :href "/bar")
{:tag :a, :attrs {:href "/foo"}, :content "A link"})
;=> {:tag :a, :attrs {:href "/foo/bar"}, :content "A link"}

traversing a vector tree

I want to traverse a vector tree that represents hiccup data structures:
[:div {:class "special"} [:btn-grp '("Hello" "Hi")]]
Then I want to dispatch on the keyword of the vector, if a multimethod has been defined for the keyword, then it would return another set of vectors, which will replace the original tag.
For example, the above structure will be transformed to:
[:div {:class "special"} [:div [:button "Hello"] [:button "Hi"]]]
The custom multimethod will receive the list ("hello" "hi") as parameters. It will then return the div containing the buttons.
How do I write a function that traverses the vector and dispatches on the keyword with everything else in the form as parameter, and then replaces the current form with the returned form ?
(ns customtags
(:require [clojure.walk :as walk]))
(def customtags (atom {}))
(defn add-custom-tag [tag f]
(swap! customtags assoc tag f))
(defn try-transform [[tag & params :as coll]]
(if-let [f (get #customtags tag)]
(apply f params)
coll))
(defmacro defcustomtag [tag params & body]
`(add-custom-tag ~tag (fn ~params ~#body)))
(defn apply-custom-tags [coll]
(walk/prewalk
(fn [x]
(if (vector? x)
(try-transform x)
x)) coll))
Using it:
(require '[customtags :as ct])
(ct/defcustomtag :btn-grp [& coll] (into [:div] (map (fn [x] [:button x]) coll)))
(ct/defcustomtag :button [name] [:input {:type "button" :id name}])
(def data [:div {:class "special"} [:btn-grp "Hello" "Hi"]])
(ct/apply-custom-tags data)
[:div {:class "special"} [:div [:input {:type "button", :id "Hello"}] [:input {:type "button", :id "Hi"}]]]