PACS System 0.1.0
PACS DICOM system library
Loading...
Searching...
No Matches
prefetch_rule_repository.cpp
Go to the documentation of this file.
1// BSD 3-Clause License
2// Copyright (c) 2021-2025, 🍀☀🌕🌥 🌊
3// See the LICENSE file in the project root for full license information.
4
13
14#include <chrono>
15#include <cstdio>
16#include <sstream>
17
18#ifdef PACS_WITH_DATABASE_SYSTEM
19
20namespace kcenon::pacs::storage {
21
22namespace {
23
24[[nodiscard]] std::string to_timestamp_string(
25 std::chrono::system_clock::time_point tp) {
26 if (tp == std::chrono::system_clock::time_point{}) {
27 return "";
28 }
29 auto time = std::chrono::system_clock::to_time_t(tp);
30 std::tm tm{};
31#ifdef _WIN32
32 gmtime_s(&tm, &time);
33#else
34 gmtime_r(&time, &tm);
35#endif
36 char buf[32];
37 std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", &tm);
38 return buf;
39}
40
41[[nodiscard]] std::chrono::system_clock::time_point from_timestamp_string(
42 const std::string& str) {
43 if (str.empty()) {
44 return {};
45 }
46 std::tm tm{};
47 if (std::sscanf(str.c_str(),
48 "%d-%d-%d %d:%d:%d",
49 &tm.tm_year,
50 &tm.tm_mon,
51 &tm.tm_mday,
52 &tm.tm_hour,
53 &tm.tm_min,
54 &tm.tm_sec) < 6) {
55 return {};
56 }
57 tm.tm_year -= 1900;
58 tm.tm_mon -= 1;
59#ifdef _WIN32
60 auto time = _mkgmtime(&tm);
61#else
62 auto time = timegm(&tm);
63#endif
64 return std::chrono::system_clock::from_time_t(time);
65}
66
67} // namespace
68
69prefetch_rule_repository::prefetch_rule_repository(
70 std::shared_ptr<pacs_database_adapter> db)
71 : base_repository(std::move(db), "prefetch_rules", "rule_id") {}
72
73auto prefetch_rule_repository::find_by_rule_id(std::string_view rule_id)
74 -> result_type {
75 return find_by_id(std::string(rule_id));
76}
77
78auto prefetch_rule_repository::find_enabled() -> list_result_type {
79 return find_where("enabled", "=", static_cast<int64_t>(1));
80}
81
82auto prefetch_rule_repository::find_by_trigger(client::prefetch_trigger trigger)
83 -> list_result_type {
84 return find_where(
85 "trigger_type", "=", std::string(client::to_string(trigger)));
86}
87
88auto prefetch_rule_repository::enable(std::string_view rule_id) -> VoidResult {
89 if (!db() || !db()->is_connected()) {
90 return VoidResult(kcenon::common::error_info{
91 -1, "Database not connected", "prefetch_rule_repository"});
92 }
93
94 std::ostringstream sql;
95 sql << "UPDATE prefetch_rules SET enabled = 1 WHERE rule_id = '"
96 << rule_id << "'";
97
98 auto result = storage_session().update(sql.str());
99 if (result.is_err()) {
100 return VoidResult(result.error());
101 }
102
103 return kcenon::common::ok();
104}
105
106auto prefetch_rule_repository::disable(std::string_view rule_id) -> VoidResult {
107 if (!db() || !db()->is_connected()) {
108 return VoidResult(kcenon::common::error_info{
109 -1, "Database not connected", "prefetch_rule_repository"});
110 }
111
112 std::ostringstream sql;
113 sql << "UPDATE prefetch_rules SET enabled = 0 WHERE rule_id = '"
114 << rule_id << "'";
115
116 auto result = storage_session().update(sql.str());
117 if (result.is_err()) {
118 return VoidResult(result.error());
119 }
120
121 return kcenon::common::ok();
122}
123
124auto prefetch_rule_repository::increment_triggered(std::string_view rule_id)
125 -> VoidResult {
126 if (!db() || !db()->is_connected()) {
127 return VoidResult(kcenon::common::error_info{
128 -1, "Database not connected", "prefetch_rule_repository"});
129 }
130
131 std::ostringstream sql;
132 sql << R"(
133 UPDATE prefetch_rules SET
134 triggered_count = triggered_count + 1,
135 last_triggered = datetime('now')
136 WHERE rule_id = ')"
137 << rule_id << "'";
138
139 auto result = storage_session().update(sql.str());
140 if (result.is_err()) {
141 return VoidResult(result.error());
142 }
143
144 return kcenon::common::ok();
145}
146
147auto prefetch_rule_repository::increment_studies_prefetched(
148 std::string_view rule_id,
149 size_t count) -> VoidResult {
150 if (!db() || !db()->is_connected()) {
151 return VoidResult(kcenon::common::error_info{
152 -1, "Database not connected", "prefetch_rule_repository"});
153 }
154
155 std::ostringstream sql;
156 sql << "UPDATE prefetch_rules SET studies_prefetched = studies_prefetched + "
157 << count << " WHERE rule_id = '" << rule_id << "'";
158
159 auto result = storage_session().update(sql.str());
160 if (result.is_err()) {
161 return VoidResult(result.error());
162 }
163
164 return kcenon::common::ok();
165}
166
167auto prefetch_rule_repository::map_row_to_entity(const database_row& row) const
168 -> client::prefetch_rule {
169 client::prefetch_rule rule;
170
171 rule.pk = std::stoll(row.at("pk"));
172 rule.rule_id = row.at("rule_id");
173 rule.name = row.at("name");
174 rule.enabled = (row.at("enabled") == "1");
175 rule.trigger = client::prefetch_trigger_from_string(row.at("trigger_type"));
176 rule.modality_filter = row.at("modality_filter");
177 rule.body_part_filter = row.at("body_part_filter");
178 rule.station_ae_filter = row.at("station_ae_filter");
179 rule.prior_lookback =
180 std::chrono::hours(std::stoll(row.at("prior_lookback_hours")));
181 rule.max_prior_studies = std::stoull(row.at("max_prior_studies"));
182 rule.prior_modalities = deserialize_vector(row.at("prior_modalities_json"));
183 rule.source_node_ids = deserialize_vector(row.at("source_node_ids_json"));
184 rule.schedule_cron = row.at("schedule_cron");
185 rule.advance_time =
186 std::chrono::minutes(std::stoll(row.at("advance_time_minutes")));
187 rule.triggered_count = std::stoull(row.at("triggered_count"));
188 rule.studies_prefetched = std::stoull(row.at("studies_prefetched"));
189 rule.last_triggered = parse_timestamp(row.at("last_triggered"));
190
191 return rule;
192}
193
194auto prefetch_rule_repository::entity_to_row(
195 const client::prefetch_rule& entity) const
196 -> std::map<std::string, database_value> {
197 return {
198 {"rule_id", entity.rule_id},
199 {"name", entity.name},
200 {"enabled", static_cast<int64_t>(entity.enabled ? 1 : 0)},
201 {"trigger_type", std::string(client::to_string(entity.trigger))},
202 {"modality_filter", entity.modality_filter},
203 {"body_part_filter", entity.body_part_filter},
204 {"station_ae_filter", entity.station_ae_filter},
205 {"prior_lookback_hours",
206 static_cast<int64_t>(entity.prior_lookback.count())},
207 {"max_prior_studies", static_cast<int64_t>(entity.max_prior_studies)},
208 {"prior_modalities_json", serialize_vector(entity.prior_modalities)},
209 {"source_node_ids_json", serialize_vector(entity.source_node_ids)},
210 {"schedule_cron", entity.schedule_cron},
211 {"advance_time_minutes",
212 static_cast<int64_t>(entity.advance_time.count())},
213 {"triggered_count", static_cast<int64_t>(entity.triggered_count)},
214 {"studies_prefetched", static_cast<int64_t>(entity.studies_prefetched)},
215 {"last_triggered", format_timestamp(entity.last_triggered)}};
216}
217
218auto prefetch_rule_repository::get_pk(const client::prefetch_rule& entity) const
219 -> std::string {
220 return entity.rule_id;
221}
222
223auto prefetch_rule_repository::has_pk(const client::prefetch_rule& entity) const
224 -> bool {
225 return !entity.rule_id.empty();
226}
227
228auto prefetch_rule_repository::select_columns() const
229 -> std::vector<std::string> {
230 return {"pk",
231 "rule_id",
232 "name",
233 "enabled",
234 "trigger_type",
235 "modality_filter",
236 "body_part_filter",
237 "station_ae_filter",
238 "prior_lookback_hours",
239 "max_prior_studies",
240 "prior_modalities_json",
241 "source_node_ids_json",
242 "schedule_cron",
243 "advance_time_minutes",
244 "triggered_count",
245 "studies_prefetched",
246 "last_triggered"};
247}
248
249auto prefetch_rule_repository::parse_timestamp(const std::string& str) const
250 -> std::chrono::system_clock::time_point {
251 return from_timestamp_string(str);
252}
253
254auto prefetch_rule_repository::format_timestamp(
255 std::chrono::system_clock::time_point tp) const -> std::string {
256 return to_timestamp_string(tp);
257}
258
259std::string prefetch_rule_repository::serialize_vector(
260 const std::vector<std::string>& vec) {
261 if (vec.empty()) return "[]";
262
263 std::ostringstream oss;
264 oss << "[";
265 for (size_t i = 0; i < vec.size(); ++i) {
266 if (i > 0) oss << ",";
267 oss << "\"";
268 for (char c : vec[i]) {
269 if (c == '"')
270 oss << "\\\"";
271 else if (c == '\\')
272 oss << "\\\\";
273 else
274 oss << c;
275 }
276 oss << "\"";
277 }
278 oss << "]";
279 return oss.str();
280}
281
282std::vector<std::string> prefetch_rule_repository::deserialize_vector(
283 std::string_view json) {
284 std::vector<std::string> result;
285 if (json.empty() || json == "[]") return result;
286
287 size_t pos = 0;
288 while (pos < json.size()) {
289 auto start = json.find('"', pos);
290 if (start == std::string_view::npos) break;
291
292 size_t end = start + 1;
293 while (end < json.size()) {
294 if (json[end] == '\\' && end + 1 < json.size()) {
295 end += 2;
296 } else if (json[end] == '"') {
297 break;
298 } else {
299 ++end;
300 }
301 }
302
303 if (end < json.size()) {
304 std::string value{json.substr(start + 1, end - start - 1)};
305 std::string unescaped;
306 for (size_t i = 0; i < value.size(); ++i) {
307 if (value[i] == '\\' && i + 1 < value.size()) {
308 unescaped += value[++i];
309 } else {
310 unescaped += value[i];
311 }
312 }
313 result.push_back(std::move(unescaped));
314 }
315
316 pos = end + 1;
317 }
318
319 return result;
320}
321
322} // namespace kcenon::pacs::storage
323
324#endif // PACS_WITH_DATABASE_SYSTEM
@ move
C-MOVE move request/response.
Repository for prefetch rules using base_repository pattern.