Skip to content

Commit d5da4d9

Browse files
CMR-7192: Redefine subscription uniqueness to use query as main parameter (#1210)
* CMR-7192 - Adding the database migration which adds the normalize column * CMR-7192 - Adding the database migration which adds the normalize column * CMR-7192 - handoff to mitch * CMR-7192: Bugfixes, test additions, general cleanup for PR * CMR-7192: Cleanup from PR feedback * CMR-7192: Test fixes * CMR-7192: Renamed function * CMR-7192: Test fixes * CMR-7192: Added index to db migration * CMR-7192: Logic and test fixes * CMR-7192: Fixed Thomas' spelling error :) * CMR-7192: Test fix * CMR-7192: Feedback from PR * CMR-7192: Test fix, added test, addressed PR comments * CMR-7192: Removed PFN constraint from subscription * CMR-7216: Removed test based on PFN-constraints * CMR-7192: Whitespace cleanup Co-authored-by: Thomas Cherry <thomas.a.cherry@nasa.gov>
1 parent 9d8bb4f commit d5da4d9

File tree

23 files changed

+300
-65
lines changed

23 files changed

+300
-65
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
(ns cmr.common-app.services.ingest.subscription-common
2+
"This contains the code for the scheduled subscription code to be shared
3+
between metadata-db and ingest."
4+
(:require
5+
[clojure.string :as string]))
6+
7+
(defn normalize-parameters
8+
"Returns a normalized url parameter string by splitting the string of parameters on '&' and
9+
sorting them alphabetically"
10+
[parameter-string]
11+
(when parameter-string
12+
(-> (if (string/starts-with? parameter-string "?")
13+
(subs parameter-string 1)
14+
parameter-string)
15+
(string/split #"&")
16+
sort
17+
(as-> $ (string/join "&" $))
18+
(string/trim))))
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
(ns cmr.common-app.test.services.ingest.subscription
2+
(:require
3+
[clojure.test :refer :all]
4+
[cmr.common-app.services.ingest.subscription-common :as sub-common]))
5+
6+
(deftest normalize-parameters-test
7+
"Query normalization, should be sorted parameters"
8+
(testing "With a leading question mark"
9+
(let [query "provider=PROV1&instrument=1B&instrument=2B"
10+
expected "instrument=1B&instrument=2B&provider=PROV1"
11+
actual (sub-common/normalize-parameters query)]
12+
(is (= expected actual))))
13+
(testing "Without a leading question mark"
14+
(let [query "provider=PROV1&instrument=1B&instrument=2B"
15+
expected "instrument=1B&instrument=2B&provider=PROV1"
16+
actual (sub-common/normalize-parameters query)]
17+
(is (= expected actual))))
18+
(testing "Empty string"
19+
(let [query ""
20+
expected ""
21+
actual (sub-common/normalize-parameters query)]
22+
(is (= expected actual)))))

ingest-app/src/cmr/ingest/api/subscriptions.clj

+55-13
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,16 @@
77
[cmr.acl.core :as acl]
88
[cmr.common-app.api.enabled :as common-enabled]
99
[cmr.common-app.api.launchpad-token-validation :as lt-validation]
10+
[cmr.common-app.services.ingest.subscription-common :as sub-common]
1011
[cmr.common.log :refer [debug info warn error]]
1112
[cmr.common.services.errors :as errors]
1213
[cmr.ingest.api.core :as api-core]
1314
[cmr.ingest.services.ingest-service :as ingest]
15+
[cmr.ingest.services.subscriptions-helper :as jobs]
1416
[cmr.ingest.validation.validation :as v]
1517
[cmr.transmit.access-control :as access-control]
1618
[cmr.transmit.metadata-db :as mdb]
19+
[cmr.transmit.metadata-db2 :as mdb2]
1720
[cmr.transmit.urs :as urs])
1821
(:import
1922
[java.util UUID]))
@@ -57,6 +60,40 @@
5760
subscriber-id
5861
concept-id)))))
5962

63+
(defn- check-duplicate-subscription
64+
"The query used by a subscriber for a collection should be unique to prevent
65+
redundent emails from being sent to them. This function will check that a
66+
subscription is unique for the following conditions: native-id, collection-id,
67+
subscriber-id, normalized-query."
68+
[request-context subscription]
69+
(let [native-id (:native-id subscription)
70+
provider-id (:provider-id subscription)
71+
metadata (-> (:metadata subscription) (json/decode true))
72+
normalized-query (:normalized-query subscription)
73+
collection-id (:CollectionConceptId metadata)
74+
subscriber-id (:SubscriberId metadata)
75+
;; Find concepts with matching collection-concept-id, normalized-query, and subscriber-id
76+
duplicate-queries (mdb/find-concepts
77+
request-context
78+
{:collection-concept-id collection-id
79+
:normalized-query normalized-query
80+
:subscriber-id subscriber-id
81+
:exclude-metadata true
82+
:latest true}
83+
:subscription)
84+
;;we only want to look at non-deleted subscriptions
85+
active-duplicate-queries (remove :deleted duplicate-queries)]
86+
;;If there is at least one duplicate subscription,
87+
;;We need to make sure it has the same native-id, or else reject the ingest
88+
(when (and (> (count active-duplicate-queries) 0)
89+
(every? #(not= native-id (:native-id %)) active-duplicate-queries))
90+
(errors/throw-service-error
91+
:conflict
92+
(format (str "The subscriber-id [%s] has already subscribed to the "
93+
"collection with concept-id [%s] using the query [%s]. "
94+
"Subscribers must use unique queries for each Collection.")
95+
subscriber-id collection-id normalized-query)))))
96+
6097
(defn- get-subscriber-id
6198
"Returns the subscriber id of the given subscription concept by parsing its metadata."
6299
[sub-concept]
@@ -154,23 +191,25 @@
154191
(get-unique-native-id context subscription))
155192
native-id)))
156193

157-
(defn- add-id-and-email-to-metadata-if-missing
194+
(defn- add-id-to-metadata-if-missing
158195
"If SubscriberId is provided, use it. Else, get it from the token."
159196
[context metadata]
160-
(let [{subscriber :SubscriberId} metadata
161-
subscriber (if-not subscriber
197+
(let [subscriber-id (:SubscriberId metadata)
198+
subscriber (if-not subscriber-id
162199
(api-core/get-user-id-from-token context)
163-
subscriber)]
200+
subscriber-id)]
164201
(assoc metadata :SubscriberId subscriber)))
165202

166-
167-
(defn add-id-and-email-if-missing
168-
"Parses and generates the metadata, such that add-id-and-email-to-metadata-if-missing
169-
can focus on insertion logic."
203+
(defn- add-fields-if-missing
204+
"Parses and generates the metadata, such that add-id-to-metadata-if-missing
205+
can focus on insertion logic. Also adds normalized-query to the concept."
170206
[context subscription]
171207
(let [metadata (json/parse-string (:metadata subscription) true)
172-
new-metadata (add-id-and-email-to-metadata-if-missing context metadata)]
173-
(assoc subscription :metadata (json/generate-string new-metadata))))
208+
new-metadata (add-id-to-metadata-if-missing context metadata)
209+
normalized (sub-common/normalize-parameters (:Query metadata))]
210+
(assoc subscription
211+
:metadata (json/generate-string new-metadata)
212+
:normalized-query normalized)))
174213

175214
(defn create-subscription
176215
"Processes a request to create a subscription. A native id will be generated."
@@ -186,9 +225,10 @@
186225
headers)
187226
native-id (get-unique-native-id request-context tmp-subscription)
188227
new-subscription (assoc tmp-subscription :native-id native-id)
189-
newer-subscription (add-id-and-email-if-missing request-context new-subscription)
228+
newer-subscription (add-fields-if-missing request-context new-subscription)
190229
subscriber-id (get-subscriber-id newer-subscription)]
191230
(check-ingest-permission request-context provider-id subscriber-id)
231+
(check-duplicate-subscription request-context newer-subscription)
192232
(perform-subscription-ingest request-context newer-subscription headers))))
193233

194234
(defn create-subscription-with-native-id
@@ -209,9 +249,10 @@
209249
body
210250
content-type
211251
headers)
212-
new-subscription (add-id-and-email-if-missing request-context tmp-subscription)
252+
new-subscription (add-fields-if-missing request-context tmp-subscription)
213253
subscriber-id (get-subscriber-id new-subscription)]
214254
(check-ingest-permission request-context provider-id subscriber-id)
255+
(check-duplicate-subscription request-context new-subscription)
215256
(perform-subscription-ingest request-context new-subscription headers))))
216257

217258
(defn create-or-update-subscription-with-native-id
@@ -236,9 +277,10 @@
236277
:latest true}
237278
:subscription))]
238279
(get-in original-subscription [:extra-fields :subscriber-id]))
239-
new-subscription (add-id-and-email-if-missing request-context tmp-subscription)
280+
new-subscription (add-fields-if-missing request-context tmp-subscription)
240281
new-subscriber (get-subscriber-id new-subscription)]
241282
(check-ingest-permission request-context provider-id new-subscriber old-subscriber)
283+
(check-duplicate-subscription request-context new-subscription)
242284
(perform-subscription-ingest request-context new-subscription headers)))
243285

244286
(defn delete-subscription

ingest-app/src/cmr/ingest/services/ingest_service/subscription.clj

+2-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111
(assoc concept :extra-fields
1212
{:subscription-name (:Name subscription)
1313
:collection-concept-id (:CollectionConceptId subscription)
14-
:subscriber-id (:SubscriberId subscription)}))
14+
:subscriber-id (:SubscriberId subscription)
15+
:normalized-query (:normalized-query concept)}))
1516

1617
(defn-timed save-subscription
1718
"Store a subscription concept in mdb and indexer."

ingest-app/src/cmr/ingest/services/subscriptions_helper.clj

+1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
[clj-time.core :as t]
66
[clojure.spec.alpha :as spec]
77
[clojure.string :as string]
8+
[cmr.common-app.services.search.params :as params]
89
[cmr.common.config :as cfg :refer [defconfig]]
910
[cmr.common.log :refer (debug info warn error)]
1011
[cmr.transmit.access-control :as access-control]

metadata-db-app/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ Supported combinations of concept type and parameters:
404404
* tag associations with concept-id, native-id, tag-key, associated-concept-id, associated-revision-id
405405
* services with concept-id, provider-id, native-id
406406
* service associations with concept-id, native-id, service-concept-id, associated-concept-id, associated-revision-id
407+
* subscriptions with collection-concept-id, provider-id, subscriber-id, native-id, concept-id, normalized-query
407408
* variables with concept-id, provider-id, native-id
408409
* variable associations with concept-id, native-id, variable-concept-id, associated-concept-id, associated-revision-id
409410

metadata-db-app/int-test/cmr/metadata_db/int_test/concepts/delete_test.clj

+1-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
(deftest delete-concepts-test
2424
(doseq [concept-type [:collection :granule :service
2525
:service-association :subscription :tool :tool-association]]
26-
(cd-spec/general-delete-test concept-type ["REG_PROV" "SMAL_PROV"])))
26+
(cd-spec/general-delete-test concept-type ["REG_PROV" "SMAL_PROV"])))
2727

2828
(deftest delete-variable-and-association-concepts-test
2929
(cd-spec/general-delete-variable-and-association-test ["REG_PROV" "SMAL_PROV"]))

metadata-db-app/int-test/cmr/metadata_db/int_test/concepts/subscription_save_test.clj

-8
Original file line numberDiff line numberDiff line change
@@ -36,14 +36,6 @@
3636
(let [concept (concepts/create-concept :subscription "PROV1" 2)]
3737
(util/concept-created-at-assertions "subscription" concept)))
3838

39-
(deftest save-subscription-with-conflicting-native-id
40-
(let [concept (concepts/create-concept :subscription "PROV1" 1)]
41-
(util/concept-with-conflicting-native-id-assertions
42-
"subscription"
43-
:subscription-name
44-
concept
45-
"sub-native-different")))
46-
4739
(deftest save-subscription-failures-test
4840
(testing "saving invalid subscription"
4941
(are3 [subscription exp-status exp-errors]

metadata-db-app/int-test/cmr/metadata_db/int_test/concepts/utils/subscription.clj

+2-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,8 @@
2222
(let [native-id (str "sub-native" uniq-num)
2323
extra-fields (merge {:subscription-name (str "subname" uniq-num)
2424
:subscriber-id (str "subid" uniq-num)
25-
:collection-concept-id "C1234-PROV1"}
25+
:collection-concept-id "C12345-PROV1"
26+
:normalized-query (str "instrument=" uniq-num "B")}
2627
(:extra-fields attributes))
2728
attributes (merge {:user-id (str "user" uniq-num)
2829
:format "application/json"

metadata-db-app/src/cmr/metadata_db/data/oracle/concepts/subscription.clj

+5-3
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
(assoc :concept-type :subscription)
1919
(assoc :provider-id (:provider_id result))
2020
(assoc :user-id (:user_id result))
21+
(assoc-in [:extra-fields :normalized-query] (:normalized_query result))
2122
(assoc-in [:extra-fields :subscription-name] (:subscription_name result))
2223
(assoc-in [:extra-fields :subscriber-id] (:subscriber_id result))
2324
(add-last-notified-at-if-present result db)
@@ -28,14 +29,15 @@
2829
[concept]
2930
(let [{{:keys [subscription-name
3031
subscriber-id
31-
collection-concept-id]} :extra-fields
32+
collection-concept-id
33+
normalized-query]} :extra-fields
3234
user-id :user-id
3335
provider-id :provider-id} concept
3436
[cols values] (concepts/concept->common-insert-args concept)]
3537
[(concat cols ["provider_id" "user_id" "subscription_name"
36-
"subscriber_id" "collection_concept_id"])
38+
"subscriber_id" "collection_concept_id" "normalized_query"])
3739
(concat values [provider-id user-id subscription-name
38-
subscriber-id collection-concept-id])]))
40+
subscriber-id collection-concept-id normalized-query])]))
3941

4042
(defmethod concepts/concept->insert-args [:subscription false]
4143
[concept _]

metadata-db-app/src/cmr/metadata_db/data/oracle/search.clj

+2-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,8 @@
3535
:humanizer (into common-columns [:user_id])
3636
:subscription (into common-columns
3737
[:provider_id :subscription_name :subscriber_id
38-
:email_address :collection_concept_id :user_id])
38+
:email_address :collection_concept_id :user_id
39+
:normalized_query])
3940
:variable (into common-columns [:provider_id :variable_name :measurement :user_id :fingerprint])
4041
:variable-association (into common-columns
4142
[:associated_concept_id :associated_revision_id
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
(ns cmr.metadata-db.migrations.074-add-normalized-to-subscription-table
2+
(:require
3+
[cheshire.core :as json]
4+
[cmr.common.util :as util]
5+
[cmr.common-app.services.ingest.subscription-common :as sub-common]
6+
[config.mdb-migrate-helper :as helper]))
7+
8+
(defn result->query
9+
"From a Database result, extract out the query and convert it to a usable string.
10+
The algorithm on how to normalize the query may change over time, for this
11+
migration version 1 will be used."
12+
[result]
13+
(-> (:metadata result)
14+
util/gzip-blob->string
15+
(as-> % (json/parse-string %, true))
16+
:Query
17+
sub-common/normalize-parameters))
18+
19+
(defn populate-new-column
20+
"Pull out the requested query from the matadata, then normalize the data and
21+
populate the new 'normalized_query' column."
22+
[]
23+
(doseq [result (helper/query "SELECT id, concept_id, metadata FROM cmr_subscriptions")]
24+
(let [id (:id result)
25+
query (result->query result)
26+
sql-statment (format "UPDATE cmr_subscriptions SET normalized_query='%s' WHERE id=%s" query id)]
27+
(helper/sql sql-statment))))
28+
29+
(defn- create-subscription-index
30+
[]
31+
(helper/sql "CREATE INDEX cmr_subs_ccisinq ON CMR_SUBSCRIPTIONS
32+
(collection_concept_id, subscriber_id, normalized_query)"))
33+
34+
(defn up
35+
"Migrates the database up to version 74."
36+
[]
37+
(println "cmr.metadata-db.migrations.074-update-subscription-table up...")
38+
39+
;; The Normalized_Query column holds a version of the Subscription Query which
40+
;; has been normalized to a standard followed by both EDSC and CMR. Both
41+
;; applications can assume the normalized query is the same despite changes
42+
;; in query order or other such nominal differences. Simple string compair
43+
;; can be used on the normalized query.
44+
(helper/sql "alter table cmr_subscriptions add normalized_query VARCHAR2(4000)")
45+
(populate-new-column)
46+
(create-subscription-index))
47+
48+
(defn down
49+
"Migrates the database down from version 74."
50+
[]
51+
(println "cmr.metadata-db.migrations.074-update-subscription-table down.")
52+
(helper/sql "DROP INDEX cmr_subs_ccisinq")
53+
(helper/sql "alter table cmr_subscriptions drop column normalized_query"))

metadata-db-app/src/cmr/metadata_db/services/concept_constraints.clj

+1-2
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,7 @@
158158
:granule (when (enforce-granule-ur-constraint) [granule-ur-unique-constraint])
159159
:acl [(unique-field-constraint :acl-identity)]
160160
:service [(partial pfn-constraint :service-name)]
161-
:tool [(partial pfn-constraint :tool-name)]
162-
:subscription [(partial pfn-constraint :subscription-name)]})
161+
:tool [(partial pfn-constraint :tool-name)]})
163162

164163
(defn perform-post-commit-constraint-checks
165164
"Perform the post commit constraint checks aggregating any constraint

metadata-db-app/src/cmr/metadata_db/services/concept_validations.clj

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
:tool {true #{}
4848
false #{:tool-name}}
4949
:subscription {true #{}
50-
false #{:subscription-name :subscriber-id :collection-concept-id}}
50+
false #{:subscription-name :subscriber-id :collection-concept-id :normalized-query}}
5151
:tag-association {true #{}
5252
false #{:associated-concept-id :associated-revision-id}}
5353
:variable {true #{}

metadata-db-app/src/cmr/metadata_db/services/search_service.clj

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
:access-group default-supported-find-parameters
2727
:acl default-supported-find-parameters
2828
:humanizer #{:concept-id :native-id}
29-
:subscription #{:provider-id :concept-id :native-id}
29+
:subscription #{:provider-id :concept-id :native-id :collection-concept-id :subscriber-id :normalized-query}
3030
:variable #{:provider-id :concept-id :native-id}
3131
:variable-association #{:concept-id :native-id :associated-concept-id :associated-revision-id
3232
:variable-concept-id}

system-int-test/src/cmr/system_int_test/utils/subscription_util.clj

+13
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
[cmr.system-int-test.data2.core :as d]
1010
[cmr.system-int-test.data2.umm-spec-subscription :as data-umm-sub]
1111
[cmr.system-int-test.system :as s]
12+
[cmr.system-int-test.utils.index-util :as index]
1213
[cmr.system-int-test.utils.ingest-util :as ingest-util]
1314
[cmr.system-int-test.utils.search-util :as search]
1415
[cmr.system-int-test.utils.url-helper :as urls]
@@ -182,3 +183,15 @@
182183
"Verifies the subscription references"
183184
[subscriptions response]
184185
(d/refs-match? subscriptions response))
186+
187+
(defn create-subscription-and-index
188+
[collection name user query]
189+
(let [concept {:Name (or name "test_sub_prov1")
190+
:SubscriberId (or user "user2")
191+
:CollectionConceptId (:concept-id collection)
192+
:Query (or query "instrument=1B")}
193+
subscription (make-subscription-concept concept)
194+
options {:token "mock-echo-system-token"}
195+
result (ingest-subscription subscription options)]
196+
(index/wait-until-indexed)
197+
result))

0 commit comments

Comments
 (0)