Compare commits
1402 Commits
Author | SHA1 | Date |
---|---|---|
|
ac3344c7f6 | |
|
ecc8f7e3ad | |
|
cbf7a58622 | |
|
1382b367d9 | |
|
5b3e3656f6 | |
|
1d3fab6184 | |
|
006ae75e2b | |
|
aa0569379b | |
|
bf68cbdedf | |
|
62738f7f16 | |
|
1e8f5c880c | |
|
488196656a | |
|
26716a51cf | |
|
908755c2c2 | |
|
d2b1dc3a41 | |
|
85d89ee79a | |
|
6194186b3e | |
|
5425a34a12 | |
|
957c0d1ba3 | |
|
ebea0fdf1c | |
|
08f549afd1 | |
|
a5d1cbced4 | |
|
0fd9d3dcfb | |
|
0515ad54c4 | |
|
69519b1ef7 | |
|
2e3b479cb1 | |
|
49214b7282 | |
|
fc430c3e1d | |
|
86a5916f0d | |
|
34b22e8d93 | |
|
300a705e0a | |
|
b64efe82d9 | |
|
6f67b06f71 | |
|
e67f598357 | |
|
312b6df5d2 | |
|
8fad544b17 | |
|
c3eaecdb8b | |
|
7c2af57a36 | |
|
8bf777a7e9 | |
|
936ff60fac | |
|
99a3006de8 | |
|
8e51e6fe10 | |
|
844d5e244b | |
|
0b57bcafc1 | |
|
3dd7d5d426 | |
|
6cca721be5 | |
|
4481537ceb | |
|
b5d873e44d | |
|
545d6aac09 | |
|
2dcd6a1dd0 | |
|
3ed65cfb0c | |
|
6597de7a98 | |
|
376f81f5c3 | |
|
1558a86249 | |
|
f8260a1c3a | |
|
50a6b168a7 | |
|
b6ceff2ecb | |
|
2de7616676 | |
|
f10aaaa357 | |
|
40b319c5de | |
|
5e922cf3ef | |
|
6a95c008e9 | |
|
dcbfd265a3 | |
|
e17b0b2975 | |
|
b45a937017 | |
|
36eed065e7 | |
|
e3379395e6 | |
|
b667aa3251 | |
|
1714efe81a | |
|
d9a72d2aaf | |
|
cfd9512728 | |
|
f6bd30db93 | |
|
e2813b2e5d | |
|
bc10bacb5a | |
|
7182a7fc41 | |
|
d0ae548277 | |
|
e568f3a4f5 | |
|
58454b4eaa | |
|
78657ee79e | |
|
3403510515 | |
|
4dc988b637 | |
|
f2348ea370 | |
|
85b200a08b | |
|
f965cbcb37 | |
|
b09e88798f | |
|
7e74f2aa3a | |
|
62ba6db457 | |
|
96cf9c7f54 | |
|
99faaf88aa | |
|
1cc851b293 | |
|
32137bfa82 | |
|
45ec4b1b77 | |
|
014f8a59da | |
|
1c4d2efafd | |
|
844374a42b | |
|
a7828e73a8 | |
|
6b6849f3a3 | |
|
495da271be | |
|
e19ccaa35d | |
|
498fd38265 | |
|
a3e2a59aeb | |
|
665dd51eb2 | |
|
a6389e89f6 | |
|
97b442ed6e | |
|
0c77c84460 | |
|
4607c62f15 | |
|
a5789038ac | |
|
e066d3f749 | |
|
5b327eeb77 | |
|
e25181982a | |
|
3c69f2f36c | |
|
345cdcfa10 | |
|
ca160cab7c | |
|
e211397d51 | |
|
5b2f1513ab | |
|
f834e11acc | |
|
ef32f11571 | |
|
07301bda3f | |
|
384953d30e | |
|
1f2d071508 | |
|
d6ebc161a9 | |
|
1fcf0e77d9 | |
|
37d76be697 | |
|
d7b591c9f9 | |
|
7f54c334da | |
|
24ef9dd290 | |
|
753667925a | |
|
87c06d9edd | |
|
85fd5e0997 | |
|
d8f6514598 | |
|
cb574d93b6 | |
|
4125ae8380 | |
|
387e5f2e3b | |
|
b6becac2c4 | |
|
922e17e677 | |
|
d61c33e466 | |
|
2239f054b9 | |
|
8359ef13bb | |
|
dbf92df33b | |
|
706565581d | |
|
9750f75d04 | |
|
de3e213ac8 | |
|
6b65e26c74 | |
|
d233480912 | |
|
69b571eda7 | |
|
f8df5fb84a | |
|
d54c68a8e9 | |
|
c550d59722 | |
|
d00e4b5b24 | |
|
c37d249776 | |
|
959e675e4c | |
|
67b34f84a3 | |
|
30b6d004aa | |
|
ecea9df932 | |
|
6c03e5d84a | |
|
31444d6c8f | |
|
dd83114c4d | |
|
2a1adca8c2 | |
|
59017977a4 | |
|
989f4ae542 | |
|
b133c2fa52 | |
|
7df9565691 | |
|
2ec7c6c7ff | |
|
698756856b | |
|
5de33c02a6 | |
|
de64eddfb3 | |
|
d95e270653 | |
|
1504d0f798 | |
|
88a778cc03 | |
|
50b45b2be4 | |
|
dd9227a924 | |
|
cdcdc143ea | |
|
3920c638a4 | |
|
4e535fd10f | |
|
4817864fd7 | |
|
90217b2083 | |
|
88baa65dd0 | |
|
e163ce1c06 | |
|
5436eb0d5d | |
|
859a36cbfa | |
|
9e782308d3 | |
|
4ba5695eee | |
|
9ab26182ea | |
|
c33ac2d9b2 | |
|
08c38fb553 | |
|
3baf0df966 | |
|
69cdc772a6 | |
|
8f3ced5907 | |
|
538140dfe7 | |
|
b7978832b7 | |
|
26e1d7fff3 | |
|
46903c6f27 | |
|
37ed6a424c | |
|
208411e723 | |
|
9a1e9abd64 | |
|
40fa173338 | |
|
cda34053f7 | |
|
99d818572a | |
|
6f36434c52 | |
|
ba9cc4b85a | |
|
7916d76635 | |
|
6874de64ce | |
|
f9fa54be49 | |
|
78adc77c23 | |
|
f7f6586d72 | |
|
34919561b7 | |
|
531fc385b6 | |
|
305e0329e7 | |
|
874e86df5c | |
|
122e82f843 | |
|
5671184e7f | |
|
6071932cb4 | |
|
0af9f2901f | |
|
a5eb21d1a2 | |
|
6c4205a008 | |
|
0b5b423bdd | |
|
882d2dd5bd | |
|
c69d3a4bd1 | |
|
640e35e853 | |
|
32a39335de | |
|
45b3995bda | |
|
b390d5f0b0 | |
|
8a1ab7ea18 | |
|
a3854d6ab1 | |
|
b997946db1 | |
|
c2a82dbdba | |
|
9274c117ab | |
|
9c92ebb1bd | |
|
d825ff8363 | |
|
ae85278c30 | |
|
3c97b7baaf | |
|
4086dea703 | |
|
a1c558f4ff | |
|
2e10d34920 | |
|
20bbb2337c | |
|
64ec68bcf5 | |
|
f1817d8fef | |
|
fc6f35e581 | |
|
c62ade3878 | |
|
6d60c962fb | |
|
992c00396c | |
|
d274cdac37 | |
|
6a7987455e | |
|
f39c4b5af5 | |
|
482a5aef10 | |
|
834f720718 | |
|
6772d3f394 | |
|
6d169f55e2 | |
|
4440cda6a5 | |
|
86e18c5d28 | |
|
9acc8612a5 | |
|
fd1c1702c6 | |
|
884f8fbf77 | |
|
b0abfd02cf | |
|
cacacc2615 | |
|
c87c6e7a76 | |
|
baaa78b7ec | |
|
64ad644bdb | |
|
c5ad1b4d4f | |
|
da725d89e0 | |
|
473be8659c | |
|
409fd042f3 | |
|
bb3272d364 | |
|
5b460ead7e | |
|
b3549a1b4a | |
|
4bbaf51753 | |
|
764d6650e6 | |
|
a0723ec2f8 | |
|
234062cf33 | |
|
13811dcf25 | |
|
3227623425 | |
|
19ed5c7c97 | |
|
e91194ae16 | |
|
bbc934c6d9 | |
|
37f0f06467 | |
|
45d0656520 | |
|
9cadc71d9d | |
|
1b4947f108 | |
|
7aa77b8614 | |
|
6d3bb69420 | |
|
273efc62a7 | |
|
9f37927eaa | |
|
92c87913ac | |
|
d5228f5ccf | |
|
1df54411b7 | |
|
a94bd37cff | |
|
1c21d2444b | |
|
a3ced47e5d | |
|
21dbd3fc4c | |
|
446e2987e9 | |
|
eb4f625561 | |
|
3a9fd60fd4 | |
|
2bb2ed3992 | |
|
d959059177 | |
|
d2cb092b09 | |
|
a2a57ab8f1 | |
|
08da9a3439 | |
|
e32a712615 | |
|
54a234519f | |
|
30eb2ce082 | |
|
309f28b520 | |
|
2dba3a737d | |
|
8621944633 | |
|
f99de6fa55 | |
|
f3163dfbd4 | |
|
d5082cd5f6 | |
|
89c7f85da4 | |
|
5c7c28706e | |
|
4e3a329c40 | |
|
d0309eaa66 | |
|
f1cee1ca4c | |
|
fd7659a46f | |
|
7a1eb9b9e9 | |
|
74958fd261 | |
|
0009e23c7b | |
|
36620f8408 | |
|
0db0a50cf4 | |
|
a08589664c | |
|
cedad9c2c6 | |
|
9fb469f8e8 | |
|
c8c70e23e8 | |
|
b08e8d5537 | |
|
59139a2186 | |
|
a432760fc9 | |
|
02eed7a32c | |
|
d6d284b6a3 | |
|
3f1cfed913 | |
|
51a3410d8e | |
|
432ec438ef | |
|
2d3be2617b | |
|
4e39b55bda | |
|
473a05784c | |
|
457da96e7b | |
|
8037e343bd | |
|
90088188c9 | |
|
b144763299 | |
|
f28cefe3b1 | |
|
a8813760d6 | |
|
98a7ed0727 | |
|
20e3a5d3fe | |
|
82a5eb5687 | |
|
4f32abaf84 | |
|
7ccc896665 | |
|
e982216f70 | |
|
fdda5e94b6 | |
|
6d38b2c5a9 | |
|
03e6604960 | |
|
16ec4e459b | |
|
aaab1598a1 | |
|
2489e40c29 | |
|
0c0c5f4ad9 | |
|
20ea6bd99d | |
|
9eb64a7471 | |
|
a0041c10e4 | |
|
e6573838a0 | |
|
1f2c5a1b2a | |
|
43f076a125 | |
|
0c8ff472f2 | |
|
16e1dec928 | |
|
26659a3eed | |
|
626f5e17c0 | |
|
d4dac274ee | |
|
c8722a2ac6 | |
|
f7d6202e13 | |
|
02f4ec1061 | |
|
4f83d61306 | |
|
3816151b87 | |
|
91fffb3560 | |
|
93f4feb818 | |
|
8765cf3440 | |
|
58635411bd | |
|
f2c37eacc2 | |
|
db1bc75cde | |
|
2393924592 | |
|
c5bace6ff2 | |
|
b919333823 | |
|
90c65666e2 | |
|
ce06eee9df | |
|
5d9780333a | |
|
a2a55e8f31 | |
|
48f0685051 | |
|
1b1e527e78 | |
|
dd8ba81f12 | |
|
52e6d2b0ee | |
|
0f94ba890e | |
|
2412f296b5 | |
|
17a58efc7e | |
|
244f216582 | |
|
37b56ac2da | |
|
ce19ac91f6 | |
|
9d69ebd94a | |
|
7cb89087da | |
|
6fc0b90610 | |
|
29901b87ea | |
|
d1478c001a | |
|
b80fd6d307 | |
|
c68f78e17b | |
|
caec6e35e9 | |
|
64082617fa | |
|
082f5746c8 | |
|
d60593fa11 | |
|
870fc27ed7 | |
|
58ead7fa91 | |
|
9f6a40ec9b | |
|
7e0c70f7c5 | |
|
1f8d577a16 | |
|
037826fe1b | |
|
78e3371c05 | |
|
dd7696f473 | |
|
90648d1c9d | |
|
9196599f30 | |
|
a4d428c83c | |
|
a3285df729 | |
|
0cc5ca1397 | |
|
98360061a8 | |
|
3f5294c734 | |
|
eb5526d75f | |
|
b772119977 | |
|
5ee38510a8 | |
|
3eed950d3c | |
|
c8450358c0 | |
|
5668987274 | |
|
ee6d8b094a | |
|
0e048c1ff5 | |
|
35d4cc23c8 | |
|
ea59e7fa58 | |
|
a07eb67865 | |
|
dd53021153 | |
|
7fea9b106c | |
|
d7ed2a5de2 | |
|
8370d4209d | |
|
32340a3e0e | |
|
b7ed041eed | |
|
a81957ff56 | |
|
b0f915bb09 | |
|
61d821ae8a | |
|
dbf967a860 | |
|
7cac1984f8 | |
|
aed4ea21d6 | |
|
13dd70f847 | |
|
a36d2ab111 | |
|
a1ceb1fbbf | |
|
00bc0609eb | |
|
5e77f8ad33 | |
|
2934195a8f | |
|
d746dc0a11 | |
|
f1ea4057fc | |
|
37121e9939 | |
|
69db287007 | |
|
b215dec8b8 | |
|
beba1bd8d6 | |
|
75b9acd791 | |
|
b4f0550a2b | |
|
fed93942b8 | |
|
4591d3f1da | |
|
ccabb1856c | |
|
4ec155d91b | |
|
1ed7fc1577 | |
|
22435a694a | |
|
6990e21b47 | |
|
89cb7479f4 | |
|
cd4c8239cd | |
|
09686741ad | |
|
41128b86fb | |
|
c574ec5c77 | |
|
d23a9115c8 | |
|
326a10ba01 | |
|
865c3bb17a | |
|
e57eafd86d | |
|
7db6d8a3d3 | |
|
cf760e4cc4 | |
|
aa7f8b97f0 | |
|
baa13313e8 | |
|
5db43ad1aa | |
|
f1b0eb154d | |
|
3d0cd62c98 | |
|
eca299b3ad | |
|
91fa8cf7d6 | |
|
1d31726e57 | |
|
efbb69a998 | |
|
9cc9241b01 | |
|
aab84b80af | |
|
2bb58d6e1b | |
|
e8568a8ea2 | |
|
7ae703e1da | |
|
f3d9a55eb7 | |
|
2f5deb1a3a | |
|
145dfc70c2 | |
|
42d2d776a8 | |
|
d0b3b35981 | |
|
62f95b651b | |
|
fc40209edc | |
|
7bff3eebe6 | |
|
e2c70d9f0b | |
|
4e87fd25a4 | |
|
32b101ed8f | |
|
44b4b35431 | |
|
0ec57a1b03 | |
|
843b420cd1 | |
|
a2c04c1b23 | |
|
616d7e7b05 | |
|
f80a303e3f | |
|
72b9ffe583 | |
|
0dbdfa386d | |
|
b128f13e0b | |
|
cc2989bb59 | |
|
9ff353e509 | |
|
8e1709944f | |
|
8928bd4ab8 | |
|
a79c0ee80a | |
|
078f94edd9 | |
|
51721c2163 | |
|
4126b511fb | |
|
a0b1d25f49 | |
|
3fc7ccf88a | |
|
59f9779f19 | |
|
d8db02f872 | |
|
7cadadb8ec | |
|
c3eb16cd75 | |
|
20ce4daa4a | |
|
f9ec8de7b6 | |
|
82c265f168 | |
|
5c0aaaa8bc | |
|
4ea74d8a0a | |
|
dd33ec82cb | |
|
402cb1bc22 | |
|
6d6bcd1dea | |
|
515f38b3c7 | |
|
b3ba6fe5a2 | |
|
4420bcaaa9 | |
|
65e2bf3895 | |
|
c363bdd1ca | |
|
dbfeb72bc5 | |
|
27c9114c58 | |
|
71098a8569 | |
|
62bbd571e7 | |
|
a84f37562e | |
|
08e170e24a | |
|
340dd9c27c | |
|
253f29da2b | |
|
767e1ef04a | |
|
0467e999ee | |
|
c3c0fd2412 | |
|
be833a07f2 | |
|
bca1f0bab6 | |
|
015961b4fc | |
|
bdc19b2cc0 | |
|
1638b8974f | |
|
08567f43c8 | |
|
e65b2a0ecb | |
|
00478ec57b | |
|
1a46aea242 | |
|
0844c8f742 | |
|
41213c7422 | |
|
2334906534 | |
|
c94a0ba802 | |
|
29e6a90b10 | |
|
09c9a5c254 | |
|
0c540f1f39 | |
|
0e3c10fd74 | |
|
23c01b9fa3 | |
|
81cba71190 | |
|
3dfbfac268 | |
|
5097d7ca0f | |
|
2962bfa179 | |
|
95cf8b4857 | |
|
cd7470dd7a | |
|
3f8c009139 | |
|
0a3e0538f5 | |
|
1a3a0b1952 | |
|
d69cf5d49b | |
|
f364ca52d8 | |
|
3728fddde8 | |
|
8be250e57c | |
|
b21125d9e2 | |
|
74c72eac90 | |
|
8edf1a636a | |
|
99c4133758 | |
|
4f952fc485 | |
|
5fe18df2d3 | |
|
c7ebef3b8f | |
|
849a6c0a9f | |
|
2cc9700af3 | |
|
82cb6f6771 | |
|
08712b488e | |
|
af5403125a | |
|
b446fdc6f6 | |
|
d7f074d79b | |
|
21b2e9bb3b | |
|
d3ae4259e5 | |
|
7e24154390 | |
|
9e6ba1da33 | |
|
dcc698951c | |
|
be948e710c | |
|
b8de9e8de2 | |
|
4cdd738f36 | |
|
f524fd5c7d | |
|
2c37cd593c | |
|
0464fa64bf | |
|
8f00248e6b | |
|
8d77aa8be2 | |
|
c1cd8f026d | |
|
dd6d406d9c | |
|
219afc1358 | |
|
8dec14fbea | |
|
dd671adfa3 | |
|
8a438c03c0 | |
|
e1b563a671 | |
|
7e236b8038 | |
|
c731b22bae | |
|
f97cdd79f5 | |
|
de5aa6420f | |
|
46d04feb4b | |
|
fb57fab7d3 | |
|
6a9a77817d | |
|
bfe3b8d312 | |
|
145bf61504 | |
|
6765b31263 | |
|
eb5c43d82c | |
|
4bedb5a402 | |
|
d51cacbff6 | |
|
675de140e4 | |
|
48a196c50d | |
|
433f94a6ea | |
|
83d7501551 | |
|
e1e15f4442 | |
|
ee49872dd5 | |
|
8b723232a1 | |
|
f4e17ccae6 | |
|
d23e4d8916 | |
|
b249bfb2ce | |
|
a763740e6b | |
|
5b2813c659 | |
|
414c1f9744 | |
|
25a387022c | |
|
1bdd2bd41e | |
|
d612c90838 | |
|
4d13a134e7 | |
|
1fa8848354 | |
|
d33c54c142 | |
|
f514960048 | |
|
0d352eb128 | |
|
b9eb22f7e6 | |
|
3e65d0b809 | |
|
6dea15c006 | |
|
c720334508 | |
|
0458328c29 | |
|
9028fb88de | |
|
cc0a9b762b | |
|
11eb151b0c | |
|
1a0f6ae120 | |
|
4628a9b3ba | |
|
4abb67aa15 | |
|
11bb3118cc | |
|
6b1a89ab63 | |
|
138309bd8d | |
|
69a32a5b1b | |
|
3ad36c94f7 | |
|
0eb0c74551 | |
|
26ac6c5675 | |
|
4b930fa530 | |
|
8e516097cf | |
|
c8e91853a1 | |
|
64ef789f04 | |
|
f9e02dc941 | |
|
8e8286225e | |
|
12ed823253 | |
|
ec66946a6f | |
|
f5d134cb47 | |
|
0c3790b0dd | |
|
f7a9d57421 | |
|
ef56006da7 | |
|
b998db2253 | |
|
911464f336 | |
|
6c8657d520 | |
|
69f95b1bfd | |
|
8c9aa707b8 | |
|
0291a11d91 | |
|
77446c973e | |
|
51599474cd | |
|
da47b7f9c0 | |
|
d5a0620f59 | |
|
302f1688b3 | |
|
202e7dd82f | |
|
1374a88006 | |
|
1fb67125ce | |
|
74352ba720 | |
|
993c4ba67b | |
|
8d6e33f334 | |
|
9c2cbe3d5d | |
|
8e9ff90985 | |
|
d2b380f9f4 | |
|
bab83aeaa9 | |
|
d8f77e6ce0 | |
|
fa5ae68e30 | |
|
2ccc8557fa | |
|
ae511d09a3 | |
|
70a7abecc9 | |
|
9d9ad85e1f | |
|
0f5ee6556a | |
|
c028fa3c15 | |
|
c894337c8e | |
|
4da6edd3b0 | |
|
692653035a | |
|
877fa07e5f | |
|
1eba495ace | |
|
27a9ac68f5 | |
|
92f8e9908c | |
|
74209fdeae | |
|
dee4cff547 | |
|
ccf00fc08d | |
|
8d7976cfa8 | |
|
ce1db8cc48 | |
|
dbe73654be | |
|
21a118da95 | |
|
7d0dbf9cb5 | |
|
f66f2d143a | |
|
9851d4ba1c | |
|
01637f040c | |
|
8e4f18c45f | |
|
6caca62ac1 | |
|
1b6e160177 | |
|
137a2410d0 | |
|
82144eba96 | |
|
bf4a9d1857 | |
|
19deb846c6 | |
|
208a166038 | |
|
671c4a3868 | |
|
c641ba6f05 | |
|
7c2c46e686 | |
|
82302776a5 | |
|
36f5832727 | |
|
99bd988f66 | |
|
3d6d974661 | |
|
a67465ac7e | |
|
a8907b3a4f | |
|
82f60cea88 | |
|
5dcc43687b | |
|
df6f8ad365 | |
|
6b30f6e036 | |
|
3fdb4c0be0 | |
|
d2ba7f91a3 | |
|
90a9d1799d | |
|
39a3d2d461 | |
|
c204a03739 | |
|
a4db19b340 | |
|
3319e55870 | |
|
07ea4c02cb | |
|
449630a3d7 | |
|
1fd11c4e5b | |
|
deeb13dc5d | |
|
425621d441 | |
|
63c8a9fb5d | |
|
ec9c6589b2 | |
|
f4c20cccab | |
|
462b3f23f1 | |
|
9c6580d962 | |
|
1ff97bce03 | |
|
763d49e414 | |
|
216bff583c | |
|
826b551a0d | |
|
2fb505808e | |
|
759fb77e24 | |
|
bd9a37fa84 | |
|
a66f644f79 | |
|
d09b5f1407 | |
|
683307a6a1 | |
|
41d3d44223 | |
|
3598ee929d | |
|
e8d800c800 | |
|
898a5305d4 | |
|
b4e8962c4d | |
|
56d9933227 | |
|
7cef6732ad | |
|
62576391fb | |
|
dbbd4f7d4e | |
|
bbb817736f | |
|
c9197d8f16 | |
|
5c16534a21 | |
|
31916e04f1 | |
|
e2114eceda | |
|
4403d1acb5 | |
|
a51ea0f07f | |
|
dfc4848786 | |
|
c2cbac0645 | |
|
7a03586318 | |
|
4c48bc719f | |
|
2f81481b1c | |
|
2b99b6cdbf | |
|
6efeb39df2 | |
|
256d115657 | |
|
a8f2566782 | |
|
a01c728c0c | |
|
35389b9545 | |
|
36ab1e2763 | |
|
6fbf7ba123 | |
|
dd7300a4ea | |
|
9bbf135e96 | |
|
26b2382231 | |
|
a22673614f | |
|
fd283ba8ee | |
|
7f66afa8e6 | |
|
b8c3aa18f8 | |
|
f845d1ca55 | |
|
4133cfd628 | |
|
4d5794e5d8 | |
|
3ab330a759 | |
|
75ff31e354 | |
|
9dc2cb4e05 | |
|
72f168b97c | |
|
2554026e58 | |
|
f838369217 | |
|
fe5a20f2c3 | |
|
fb4d369615 | |
|
799a5bae5b | |
|
c625721132 | |
|
7bd35c1069 | |
|
a0190b4311 | |
|
ed9ffb307b | |
|
1a7f7de6e4 | |
|
b9312cfd43 | |
|
39a2c18b2e | |
|
8c595b0227 | |
|
b77e44e983 | |
|
8b5d8a5f31 | |
|
3a1d13842e | |
|
8eff8d9eff | |
|
ed415a7fcf | |
|
5737439269 | |
|
c1dec9e6c5 | |
|
15dae81665 | |
|
e99f7c44f7 | |
|
5d35c9dcf2 | |
|
c2da0b1631 | |
|
899191a171 | |
|
1fb173db66 | |
|
b6d4e7eec0 | |
|
a671984dbd | |
|
ab185168bb | |
|
ac784779ce | |
|
f86899b108 | |
|
d5a9867365 | |
|
ead77c113c | |
|
f819ead577 | |
|
59a792f836 | |
|
059572a3d8 | |
|
23bb4974aa | |
|
9684687c2d | |
|
210a9dcc06 | |
|
a0342b7e20 | |
|
7e100de6d6 | |
|
59c1c277aa | |
|
ee8a411026 | |
|
514d463245 | |
|
f887fe8656 | |
|
818131b77e | |
|
bdddeb19b2 | |
|
41e9ebb818 | |
|
f0734f7c91 | |
|
7a7c7e868f | |
|
27fbe4521c | |
|
c05609ae4c | |
|
af55f21687 | |
|
29efc6c62f | |
|
6fd752d59d | |
|
fd774e3547 | |
|
692c368d74 | |
|
693b0ba4b2 | |
|
68f97c6c08 | |
|
5fd86b6444 | |
|
c2d2e0dedd | |
|
c66b99579f | |
|
4c686c9623 | |
|
1441c2c12e | |
|
d063bf292c | |
|
a2e4894807 | |
|
1ad0129a79 | |
|
2763c8f7a6 | |
|
1aea7843ba | |
|
742ad0bc17 | |
|
e92cfda3a9 | |
|
84f72ac70d | |
|
9ae440786f | |
|
91f7552745 | |
|
8a38d12360 | |
|
a4113c6308 | |
|
30942afe84 | |
|
816950a895 | |
|
2a0da511a7 | |
|
3142c43c05 | |
|
506e89fd34 | |
|
7b1eb1c035 | |
|
300c5054b9 | |
|
dffa593440 | |
|
49e49cc9e4 | |
|
46bbdccf0f | |
|
e260022cf5 | |
|
5d4230f0fd | |
|
e423726254 | |
|
666f784c2e | |
|
9e146330df | |
|
04adc3eb4d | |
|
adfb58764a | |
|
703551f54c | |
|
4a4d81367b | |
|
3496366ae8 | |
|
1f28921fec | |
|
94a5a869d8 | |
|
a741568762 | |
|
6c52ee464f | |
|
350196c688 | |
|
b17a20e43f | |
|
fb42a92e9b | |
|
a6eabc391d | |
|
d8c64d91c4 | |
|
644afb09c1 | |
|
4d04996571 | |
|
6ce68de437 | |
|
a73083def4 | |
|
5985005dda | |
|
0336c02eeb | |
|
9f03c0ea32 | |
|
37fd2be673 | |
|
8789f90d33 | |
|
4972291276 | |
|
c545ed5680 | |
|
0ecf7b5984 | |
|
7d4981f429 | |
|
18b4fc0958 | |
|
0676f4d24b | |
|
f0126043f2 | |
|
90c931acd9 | |
|
323083d7e9 | |
|
fc034c3284 | |
|
6ccefbb8bc | |
|
5c4bc86139 | |
|
b8194ec13e | |
|
3a8e262da4 | |
|
6590d84f6e | |
|
496c4d0332 | |
|
b208c23362 | |
|
a2ee70e8d6 | |
|
965bb19410 | |
|
dd7d02466c | |
|
d1596924f0 | |
|
b4bae84bc1 | |
|
fe3be7df73 | |
|
7993c360d6 | |
|
fd2262f058 | |
|
ef199b94fd | |
|
e9044af48e | |
|
6253aee3b3 | |
|
bbb4008b38 | |
|
4e1675d9c3 | |
|
9a3a07f774 | |
|
c115e96ae6 | |
|
40d1f0a1d5 | |
|
bad5b0a7f5 | |
|
893d0da612 | |
|
00c8120459 | |
|
2d3b644896 | |
|
b0201c7d43 | |
|
4f6d7ff46d | |
|
da00304083 | |
|
10bee74d16 | |
|
2f028f6990 | |
|
9ddc9f1cb2 | |
|
a7c7d42879 | |
|
a8e34100a0 | |
|
331d5110da | |
|
207a221d46 | |
|
693721e36c | |
|
8b9e050092 | |
|
7f01deda5b | |
|
6cd208198c | |
|
6cd588b87a | |
|
dac79f0bd5 | |
|
c3528da702 | |
|
b8b927ef4a | |
|
ea54bff9cc | |
|
bd3042ba0d | |
|
ecfeddf0f6 | |
|
3501425f48 | |
|
ec6d44ae89 | |
|
d309d16330 | |
|
6d456ca618 | |
|
fa94c0e0dd | |
|
fe42073385 | |
|
31f2148264 | |
|
93dde1d259 | |
|
3946211c5d | |
|
89cedb9d2e | |
|
56c1bbc820 | |
|
5888aead97 | |
|
6107e91be4 | |
|
3ed40a3887 | |
|
5f173ff860 | |
|
24f092319d | |
|
736cf24cbf | |
|
7f9194231c | |
|
c26b75593e | |
|
0b1315eaaf | |
|
401d7a8a5f | |
|
e1a0432ae9 | |
|
cc155b354c | |
|
1dddd68c42 | |
|
58df782b76 | |
|
fa1e14451d | |
|
79222e1cf7 | |
|
8d675ca387 | |
|
dfb214c52f | |
|
eb6f9e69ef | |
|
e393b64715 | |
|
f9523ecd8b | |
|
5388fa12b6 | |
|
bcbaff8e4f | |
|
d4c43d74bc | |
|
1af8e966a4 | |
|
c3e82e97dd | |
|
468c42d4e3 | |
|
88e7d6054f | |
|
4d655900d9 | |
|
3ff9995a43 | |
|
aa495b2847 | |
|
106df4661d | |
|
6405100b27 | |
|
59d8a10ba3 | |
|
20628a2305 | |
|
0ccf337384 | |
|
839fddb927 | |
|
64f79cd513 | |
|
af3e3d60dc | |
|
7aade9a875 | |
|
42b931776a | |
|
b7dd2fc5a2 | |
|
434da5a608 | |
|
a5c93aca0a | |
|
19415edb71 | |
|
0de764db19 | |
|
9d7d3d41f6 | |
|
f0f5d28416 | |
|
854d0be0f4 | |
|
5b10d399cb | |
|
be9d6523ff | |
|
2df3205c74 | |
|
5abe971bdb | |
|
c426e6646f | |
|
cd27e38f67 | |
|
a5f076b37c | |
|
6898514fca | |
|
311b73fe35 | |
|
212590e5e2 | |
|
c92cd2ccfe | |
|
427d5a6272 | |
|
30155712bc | |
|
b55ed6cf74 | |
|
a5eaf79cf9 | |
|
2ff65b8344 | |
|
28e191d423 | |
|
43a75d080c | |
|
0f76349a50 | |
|
813c7e21ab | |
|
9c98e83ed6 | |
|
519c32a087 | |
|
f3e65db54e | |
|
702957c517 | |
|
f1ecfac6aa | |
|
5072553166 | |
|
ade4878abc | |
|
f51d0201c6 | |
|
22828d1d3f | |
|
a737c3a36b | |
|
037d61128e | |
|
ec3111f5d7 | |
|
9b77827796 | |
|
ae307892a5 | |
|
5c335d4539 | |
|
23c4c4cef9 | |
|
bdb08d7af8 | |
|
6a8c911287 | |
|
dfa08b90e1 | |
|
0ce5b43a81 | |
|
d7b3ca0513 | |
|
5e648f6332 | |
|
bac2af3033 | |
|
6d7c43d120 | |
|
3ae2a541a1 | |
|
de9a928f93 | |
|
f5ceeb06b4 | |
|
cfd70863cc | |
|
996cbd853a | |
|
5195f375f9 | |
|
5c6777f4b9 | |
|
09b50536fb | |
|
074066dfb3 | |
|
ecc2ad2cb3 | |
|
c1262a2e0d | |
|
e4e9ecb16d | |
|
b26dd5833d | |
|
8d22a64f04 | |
|
8079274cf9 | |
|
3d0ba8bb9a | |
|
eac212d4ba | |
|
d8e7d9e10c | |
|
77890303c3 | |
|
aec31b1a68 | |
|
9df00dd489 | |
|
66f995d550 | |
|
d72b068559 | |
|
f6363e3d4b | |
|
aee10c3a3d | |
|
d3ef8927a2 | |
|
61a87cf408 | |
|
dfff114b15 | |
|
953b48041a | |
|
fa613fcddc | |
|
c8d14dedb1 | |
|
d4480382ca | |
|
33bf772158 | |
|
c7c9546a29 | |
|
f19d5d7083 | |
|
c92c271f09 | |
|
cbd4618871 | |
|
33ee57a862 | |
|
7753bfec07 | |
|
0f34a95174 | |
|
71c5fc2c0f | |
|
c4d7b71f09 | |
|
ec8f129ffc | |
|
20a9da6194 | |
|
cb18a099c5 | |
|
0a27a77fc1 | |
|
09824e7c52 | |
|
db114507da | |
|
8ee4d89c0b | |
|
80fe25ae28 | |
|
579f8ab750 | |
|
7b79bc818a | |
|
44443e4e54 | |
|
1d731f6fa3 | |
|
a3a9d0eafd | |
|
a8caae6e29 | |
|
69a1a8fad3 | |
|
883a4cd515 | |
|
259a749a5f | |
|
45d3c0fcc1 | |
|
59429ed55d | |
|
464820d5da | |
|
7459eaa028 | |
|
4296aa48bd | |
|
4098bc86e8 | |
|
514a99e5ad | |
|
7a19ac84ce | |
|
1b72aeb0e0 | |
|
c9f5899128 | |
|
237a0bcbba | |
|
d78c99ce16 | |
|
4102495dd2 | |
|
07ecea1bf9 | |
|
24c237e228 | |
|
7695883418 | |
|
f41fe211bd | |
|
bd4e12e16f | |
|
fe8698feb0 | |
|
ed2e963a2a | |
|
258b60666d | |
|
c004e757cc | |
|
e054d414e8 | |
|
93499975d0 | |
|
648518d0e0 | |
|
aad036a011 | |
|
8845242330 | |
|
8a3455cbc6 | |
|
036ac25ee3 | |
|
6c14d87c2e | |
|
43aea3b505 | |
|
b14094f9f0 | |
|
eed5d64f63 | |
|
6cb3fe485b | |
|
4d6851a237 | |
|
a08c684e84 | |
|
cd288e1b26 | |
|
c9c1445e40 | |
|
01dbff9c6d | |
|
229cd61116 | |
|
7524aea473 | |
|
f14ca34f58 | |
|
3a1d6a8ffb | |
|
5ee7675f3e | |
|
618c90beab | |
|
99dc562893 | |
|
b2b41851ce | |
|
1b822341d6 | |
|
6103dd2d39 | |
|
da55f024bf | |
|
b79a6535b6 | |
|
7451f696c8 | |
|
5c78acbbe0 | |
|
e30c21e992 | |
|
d069a8fa9d | |
|
19b5946a19 | |
|
d688685417 | |
|
b5c1e4cc84 | |
|
08e6359108 | |
|
44b5eccb5b | |
|
52e3d02426 | |
|
229b46ea49 | |
|
c2b5689e56 | |
|
4aaedfa2c3 | |
|
7312b9e265 | |
|
75bd6538cf | |
|
67b15c6e10 | |
|
c1bd636bb6 | |
|
1a3115fb36 | |
|
6af8e92df2 | |
|
f8ac46f00a | |
|
5e53fbf048 | |
|
9254b141f9 | |
|
1d0e24a8e9 | |
|
9e199342fd | |
|
44b1b2445d | |
|
f6e176a7f0 | |
|
4e5a39f670 | |
|
3b1ce92bb6 | |
|
edb09f0c3e | |
|
8303ae4947 | |
|
801992e50c | |
|
a2afa5cce0 | |
|
6e3e1853a2 | |
|
e399174371 | |
|
a03ae8d252 | |
|
53bc865dc5 | |
|
5120778010 | |
|
1a69c64a76 | |
|
c132322093 | |
|
25511fd139 | |
|
20291b6add | |
|
f25b6cedd5 | |
|
5da2cccd95 | |
|
77867620a9 | |
|
3c560897f4 | |
|
591e23bff9 | |
|
0544597511 | |
|
cc4bcb4b57 | |
|
a05ef562b4 | |
|
1a283b3e19 | |
|
51a38c64d4 | |
|
519f92e2c7 | |
|
5048074625 | |
|
37091bd03d | |
|
ba78048ae3 | |
|
60d2cbbc14 | |
|
1c4a821315 | |
|
b886c63a2f | |
|
17504af1b0 | |
|
4a437d5776 | |
|
c3e2de779c | |
|
72fe979cfb | |
|
1a40c97d23 | |
|
1b59dc4d3d | |
|
bf985b9bdd | |
|
79cc1250fb | |
|
02d9cb9a65 | |
|
727e01340c | |
|
ea14348edc | |
|
d6c45c3f07 | |
|
ca68ae7871 | |
|
7f06b9af83 | |
|
fcc8972022 | |
|
785470213e | |
|
113b5e5f2e | |
|
2761e661b5 | |
|
5a01ed0ba1 | |
|
c9ba9c9275 | |
|
4b0fa1b4b5 | |
|
cc44618537 | |
|
d67932052b | |
|
f0a82975da | |
|
6a299d7228 | |
|
27661615f7 | |
|
3af68113dd | |
|
fad0f35fc8 | |
|
54fbf081ed | |
|
5da918205b | |
|
d7fabaf003 | |
|
118fe7dec2 | |
|
a33030c545 | |
|
125b9c1bde | |
|
60341ff163 | |
|
b063a621ab | |
|
90f569991b | |
|
2192932863 | |
|
71a5699f18 | |
|
e04de2ae46 | |
|
64a9affa92 | |
|
348e3ebb96 | |
|
bb36557f52 | |
|
5fb7c1f8be | |
|
64ec731068 | |
|
b8a0850e10 | |
|
da7a662334 | |
|
ce89ecf1e7 | |
|
e7ba3f65e8 | |
|
2c0190793f | |
|
e52fe0bae5 | |
|
73cc784491 | |
|
9647c3f04d | |
|
e9732b582d | |
|
3788a3be8e | |
|
faca91764f | |
|
f245ef5e95 | |
|
29f32939f8 | |
|
6eeeddd2ea | |
|
715fd032cf | |
|
a79ae45a4a | |
|
26a14ab2d4 | |
|
819a98defb | |
|
40c521913a | |
|
ba1c528fca | |
|
5f850fa3d8 | |
|
6a7012ec3f | |
|
3d1a2337b6 | |
|
5e96d140c1 | |
|
2579fc6a80 | |
|
8ca655a788 | |
|
7556d5278d | |
|
7bd7815d30 | |
|
c4210f1607 | |
|
0be28a007b | |
|
475f3c02c1 | |
|
da788c2a51 | |
|
84f220d813 | |
|
e108666a27 | |
|
f8d62eea2f | |
|
2eec1a5519 | |
|
5db90f1a38 | |
|
9ec42f9869 | |
|
81ab0710ea | |
|
00af2f88f8 | |
|
887b62951a | |
|
7e38e546b3 | |
|
89ea36363f | |
|
60fad0507a | |
|
5d262470e8 | |
|
3e62414222 | |
|
0152a1eef9 | |
|
35c9c7e06f | |
|
4fa0a08389 | |
|
b626789693 | |
|
7e6259c14f | |
|
c701a6c4eb | |
|
0d6ae8c132 | |
|
d5bd0fbf02 | |
|
878ac937d2 | |
|
68bc16b3c6 | |
|
a38c2d2321 | |
|
56ca085dd8 | |
|
b5687268d7 | |
|
171f7f33f3 | |
|
852de680cf | |
|
3886182440 | |
|
b2c986f258 | |
|
6a85cd328f | |
|
fe2a361e05 | |
|
fe59a2354a | |
|
659075b666 | |
|
f5e81e631c | |
|
4efb032eea | |
|
a65e6471b7 | |
|
99d43eb6db | |
|
da4901acda | |
|
aa120bdc51 | |
|
7fa39bcd04 | |
|
50225388af | |
|
4ec5258d95 | |
|
52c97309d2 | |
|
3cf5a6ffbb | |
|
405e812687 | |
|
4662444f7a | |
|
c86b4c1886 | |
|
8ad85bcfe3 | |
|
aeb14f61e3 | |
|
2ad492a79c | |
|
2e5b26f2e4 | |
|
84dbd4c819 | |
|
6eed1a3107 | |
|
b81b944561 | |
|
a862325354 | |
|
a8cffb0a91 | |
|
f36b641395 | |
|
360d2b7013 | |
|
465d1139d5 | |
|
3051790370 | |
|
960af76097 | |
|
cb189f1c2a | |
|
a307cea536 | |
|
c75d556858 | |
|
4dd0017e89 | |
|
e4093c2482 | |
|
d1c39316e2 | |
|
e7b2a4503b | |
|
54faaf99ae |
|
@ -0,0 +1,12 @@
|
|||
|
||||
# CLOMonitor metadata file
|
||||
# This file must be located at the root of the repository
|
||||
|
||||
# Checks exemptions
|
||||
|
||||
# Check identifiers are here https://github.com/cncf/clomonitor/blob/main/docs/checks.md#exemptions (look for "id")
|
||||
exemptions:
|
||||
- check: signed_releases
|
||||
reason: "Our releases are signed on Maven Central"
|
||||
- check: artifacthub_badge
|
||||
reason: "Java library, not a k8s thing. We use Maven Central"
|
|
@ -0,0 +1,72 @@
|
|||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_size = 4
|
||||
indent_style = space
|
||||
insert_final_newline = true
|
||||
tab_width = 4
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
ij_continuation_indent_size = 8
|
||||
|
||||
[*.md]
|
||||
max_line_length = off
|
||||
trim_trailing_whitespace = false
|
||||
|
||||
# Following the rules of the Google Java Style Guide.
|
||||
# See https://google.github.io/styleguide/javaguide.html
|
||||
[*.java]
|
||||
max_line_length = 120
|
||||
|
||||
ij_java_do_not_wrap_after_single_annotation_in_parameter = true
|
||||
ij_java_insert_inner_class_imports = false
|
||||
ij_java_class_count_to_use_import_on_demand = 999
|
||||
ij_java_names_count_to_use_import_on_demand = 999
|
||||
ij_java_packages_to_use_import_on_demand = unset
|
||||
ij_java_imports_layout = $*,|,*
|
||||
ij_java_doc_align_param_comments = true
|
||||
ij_java_doc_align_exception_comments = true
|
||||
ij_java_doc_add_p_tag_on_empty_lines = false
|
||||
ij_java_doc_do_not_wrap_if_one_line = true
|
||||
ij_java_doc_keep_empty_parameter_tag = false
|
||||
ij_java_doc_keep_empty_throws_tag = false
|
||||
ij_java_doc_keep_empty_return_tag = false
|
||||
ij_java_doc_preserve_line_breaks = true
|
||||
ij_java_doc_indent_on_continuation = true
|
||||
ij_java_keep_control_statement_in_one_line = false
|
||||
ij_java_keep_blank_lines_in_code = 1
|
||||
ij_java_align_multiline_parameters = false
|
||||
ij_java_align_multiline_resources = false
|
||||
ij_java_align_multiline_for = true
|
||||
ij_java_space_before_array_initializer_left_brace = true
|
||||
ij_java_call_parameters_wrap = normal
|
||||
ij_java_method_parameters_wrap = normal
|
||||
ij_java_extends_list_wrap = normal
|
||||
ij_java_throws_keyword_wrap = normal
|
||||
ij_java_method_call_chain_wrap = normal
|
||||
ij_java_binary_operation_wrap = normal
|
||||
ij_java_binary_operation_sign_on_next_line = true
|
||||
ij_java_ternary_operation_wrap = normal
|
||||
ij_java_ternary_operation_signs_on_next_line = true
|
||||
ij_java_keep_simple_methods_in_one_line = true
|
||||
ij_java_keep_simple_lambdas_in_one_line = true
|
||||
ij_java_keep_simple_classes_in_one_line = true
|
||||
ij_java_for_statement_wrap = normal
|
||||
ij_java_array_initializer_wrap = normal
|
||||
ij_java_wrap_comments = true
|
||||
ij_java_if_brace_force = always
|
||||
ij_java_do_while_brace_force = always
|
||||
ij_java_while_brace_force = always
|
||||
ij_java_for_brace_force = always
|
||||
ij_java_space_after_closing_angle_bracket_in_type_argument = false
|
||||
|
||||
[{*.json,*.json5}]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
ij_smart_tabs = false
|
||||
|
||||
[*.yaml]
|
||||
indent_size = 2
|
||||
tab_width = 2
|
|
@ -3,4 +3,3 @@
|
|||
#
|
||||
# These are explicitly windows files and should use crlf
|
||||
*.bat text eol=crlf
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
name: ClusterFuzzLite batch fuzzing
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# │ │ │ │ │
|
||||
# │ │ │ │ │
|
||||
# │ │ │ │ │
|
||||
# * * * * *
|
||||
- cron: '0 0 * * *' # Every 6th hour. Change this to whatever is suitable.
|
||||
permissions: read-all
|
||||
jobs:
|
||||
BatchFuzzing:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
sanitizer:
|
||||
- address
|
||||
- undefined
|
||||
steps:
|
||||
- name: Build Fuzzers (${{ matrix.sanitizer }})
|
||||
id: build
|
||||
uses: google/clusterfuzzlite/actions/build_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
|
||||
with:
|
||||
language: jvm
|
||||
sanitizer: ${{ matrix.sanitizer }}
|
||||
- name: Run Fuzzers (${{ matrix.sanitizer }})
|
||||
id: run
|
||||
uses: google/clusterfuzzlite/actions/run_fuzzers@884713a6c30a92e5e8544c39945cd7cb630abcd1 # v1
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
fuzz-seconds: 3600
|
||||
mode: 'batch'
|
||||
sanitizer: ${{ matrix.sanitizer }}
|
|
@ -0,0 +1,23 @@
|
|||
name: 'Lint PR'
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types:
|
||||
- opened
|
||||
- edited
|
||||
- synchronize
|
||||
|
||||
permissions: # added using https://github.com/step-security/secure-workflows
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
main:
|
||||
permissions:
|
||||
pull-requests: read # for amannn/action-semantic-pull-request to analyze PRs
|
||||
statuses: write # for amannn/action-semantic-pull-request to mark status of analyzed PR
|
||||
name: Validate PR title
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: amannn/action-semantic-pull-request@335288255954904a41ddda8947c8f2c844b8bfeb
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
@ -9,36 +9,36 @@ name: on-merge
|
|||
|
||||
on:
|
||||
push:
|
||||
branches: [ master, main ]
|
||||
branches:
|
||||
- main
|
||||
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
environment: publish
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 8
|
||||
uses: actions/setup-java@v3
|
||||
- uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@ae2b61dbc685e60e4427b2e8ed4f0135c6ea8597
|
||||
with:
|
||||
java-version: '8'
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: maven
|
||||
server-id: ossrh
|
||||
server-username: ${{ secrets.OSSRH_USERNAME }}
|
||||
server-password: ${{ secrets.OSSRH_PASSWORD }}
|
||||
server-id: central
|
||||
server-username: ${{ secrets.CENTRAL_USERNAME }}
|
||||
server-password: ${{ secrets.CENTRAL_PASSWORD }}
|
||||
|
||||
- name: Cache local Maven repository
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@640a1c2554105b57832a23eea0b4672fc7a790d5
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
key: ${{ runner.os }}-17-maven-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-maven-
|
||||
${{ runner.os }}-17-maven-
|
||||
|
||||
- name: Configure GPG Key
|
||||
run: |
|
||||
|
@ -50,18 +50,22 @@ jobs:
|
|||
run: mvn --batch-mode --update-snapshots verify
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||
flags: unittests # optional
|
||||
name: coverage # optional
|
||||
fail_ci_if_error: true # optional (default = false)
|
||||
verbose: true # optional (default = false)
|
||||
|
||||
# Add -SNAPSHOT before deploy
|
||||
- name: Add SNAPSHOT
|
||||
run: mvn versions:set -DnewVersion='${project.version}-SNAPSHOT'
|
||||
|
||||
- name: Deploy
|
||||
run: |
|
||||
mvn -P gpg_verify \
|
||||
--no-transfer-progress \
|
||||
--batch-mode \
|
||||
--file pom.xml -s release/m2-settings.xml verify deploy
|
||||
mvn --batch-mode \
|
||||
--settings release/m2-settings.xml -DskipTests clean deploy
|
||||
env:
|
||||
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
|
||||
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
|
||||
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
|
||||
CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
|
||||
|
|
|
@ -3,49 +3,56 @@ on:
|
|||
pull_request:
|
||||
branches: [ master, main ]
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
build:
|
||||
- java: 17
|
||||
profile: codequality
|
||||
- java: 11
|
||||
profile: java11
|
||||
name: with Java ${{ matrix.build.java }}
|
||||
runs-on: ${{ matrix.os}}
|
||||
steps:
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v3
|
||||
- name: Set up JDK 8
|
||||
uses: actions/setup-java@v3
|
||||
uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
|
||||
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@ae2b61dbc685e60e4427b2e8ed4f0135c6ea8597
|
||||
with:
|
||||
java-version: '8'
|
||||
distribution: 'temurin'
|
||||
cache: maven
|
||||
java-version: ${{ matrix.build.java }}
|
||||
distribution: 'temurin'
|
||||
cache: maven
|
||||
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@7710ed11e398ea99c7f7004c2b2e0f580458db42
|
||||
with:
|
||||
languages: java
|
||||
|
||||
- name: Cache local Maven repository
|
||||
uses: actions/cache@v2
|
||||
uses: actions/cache@640a1c2554105b57832a23eea0b4672fc7a790d5
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-maven-
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}${{ matrix.build.java }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}${{ matrix.build.java }}-maven-
|
||||
|
||||
- name: Configure GPG Key
|
||||
if: github.event.pull_request.head.repo.full_name == github.repository
|
||||
run: |
|
||||
echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --import
|
||||
env:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
|
||||
- name: Verify with Maven
|
||||
run: mvn --batch-mode --update-snapshots --activate-profiles e2e,${{ matrix.build.profile }} verify
|
||||
|
||||
- name: Build with Maven
|
||||
run: |
|
||||
if [ ${{ github.event.pull_request.head.repo.full_name }} = ${{ github.repository }} ]; then
|
||||
mvn --batch-mode --update-snapshots verify
|
||||
else
|
||||
mvn --batch-mode --update-snapshots verify -Dgpg.skip
|
||||
fi
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
- if: matrix.build.java == '17'
|
||||
name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v5.4.3
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos
|
||||
flags: unittests # optional
|
||||
name: coverage # optional
|
||||
fail_ci_if_error: true # optional (default = false)
|
||||
verbose: true # optional (default = false)
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@7710ed11e398ea99c7f7004c2b2e0f580458db42
|
||||
|
|
|
@ -1,60 +1,64 @@
|
|||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
# This workflow will build a package using Gradle and then publish it to GitHub packages when a release is created
|
||||
# For more information see: https://github.com/actions/setup-java/blob/main/docs/advanced-usage.md#Publishing-using-gradle
|
||||
|
||||
# maven deploy commands via via https://tech.clevertap.com/automate-releases-to-maven-central-via-github-actions/
|
||||
|
||||
name: Release
|
||||
# This workflow creates a running release please PR, which tracks all changes
|
||||
# based on semantic PR titles. When that PR is merged, a publish occurs after
|
||||
# release please increments the version.
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
name: Run Release Please
|
||||
permissions: # added using https://github.com/step-security/secure-workflows
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
build:
|
||||
release-please:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write # for googleapis/release-please-action to create release commit
|
||||
pull-requests: write # for googleapis/release-please-action to create release PR
|
||||
issues: write # for googleapis/release-please-action to create labels
|
||||
|
||||
# Release-please creates a PR that tracks all changes
|
||||
steps:
|
||||
- uses: googleapis/release-please-action@v4
|
||||
id: release
|
||||
with:
|
||||
token: ${{secrets.RELEASE_PLEASE_ACTION_TOKEN}}
|
||||
outputs:
|
||||
release_created: ${{ fromJSON(steps.release.outputs.paths_released)[0] != null }} # if we have a single release path, do the release
|
||||
|
||||
publish:
|
||||
environment: publish
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
needs: release-please
|
||||
if: ${{ fromJSON(needs.release-please.outputs.release_created || false) }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up JDK 8
|
||||
uses: actions/setup-java@v3
|
||||
with:
|
||||
java-version: '8'
|
||||
distribution: 'temurin'
|
||||
cache: maven
|
||||
server-id: ossrh
|
||||
server-username: ${{ secrets.OSSRH_USERNAME }}
|
||||
server-password: ${{ secrets.OSSRH_PASSWORD }}
|
||||
- name: Checkout Repository
|
||||
uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
|
||||
|
||||
- name: Configure GPG Key
|
||||
run: |
|
||||
echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --import
|
||||
env:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
|
||||
- name: Set up JDK 17
|
||||
uses: actions/setup-java@ae2b61dbc685e60e4427b2e8ed4f0135c6ea8597
|
||||
with:
|
||||
java-version: '17'
|
||||
distribution: 'temurin'
|
||||
cache: maven
|
||||
server-id: central
|
||||
server-username: ${{ secrets.CENTRAL_USERNAME }}
|
||||
server-password: ${{ secrets.CENTRAL_PASSWORD }}
|
||||
|
||||
- name: Build with Maven
|
||||
run: mvn --batch-mode --update-snapshots verify -Dversion.modifier=''
|
||||
- name: Configure GPG Key
|
||||
run: |
|
||||
echo -n "$GPG_SIGNING_KEY" | base64 --decode | gpg --import
|
||||
env:
|
||||
GPG_SIGNING_KEY: ${{ secrets.GPG_SIGNING_KEY }}
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
flags: unittests # optional
|
||||
name: coverage # optional
|
||||
fail_ci_if_error: true # optional (default = false)
|
||||
verbose: true # optional (default = false)
|
||||
- name: Deploy
|
||||
run: |
|
||||
mvn -P gpg_verify \
|
||||
--no-transfer-progress \
|
||||
--batch-mode \
|
||||
--file pom.xml -s release/m2-settings.xml verify deploy -Dversion.modifier=''
|
||||
env:
|
||||
OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }}
|
||||
OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }}
|
||||
- name: Deploy
|
||||
run: |
|
||||
mvn --batch-mode \
|
||||
--settings release/m2-settings.xml -DskipTests clean deploy
|
||||
env:
|
||||
CENTRAL_USERNAME: ${{ secrets.CENTRAL_USERNAME }}
|
||||
CENTRAL_PASSWORD: ${{ secrets.CENTRAL_PASSWORD }}
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
name: "Code Scanning - Action"
|
||||
|
||||
# Docs for this at https://github.com/github/codeql-action#usage
|
||||
|
||||
on:
|
||||
schedule:
|
||||
# ┌───────────── minute (0 - 59)
|
||||
# │ ┌───────────── hour (0 - 23)
|
||||
# │ │ ┌───────────── day of the month (1 - 31)
|
||||
# │ │ │ ┌───────────── month (1 - 12 or JAN-DEC)
|
||||
# │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT)
|
||||
# │ │ │ │ │
|
||||
# │ │ │ │ │
|
||||
# │ │ │ │ │
|
||||
# * * * * *
|
||||
- cron: '30 1 * * 1'
|
||||
|
||||
permissions: # added using https://github.com/step-security/secure-workflows
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
CodeQL-Build:
|
||||
# CodeQL runs on ubuntu-latest, windows-latest, and macos-latest
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
permissions:
|
||||
# required for all workflows
|
||||
security-events: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@09d2acae674a48949e3602304ab46fd20ae0c42f
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@7710ed11e398ea99c7f7004c2b2e0f580458db42
|
||||
with:
|
||||
languages: java
|
||||
|
||||
- name: Autobuild
|
||||
uses: github/codeql-action/autobuild@7710ed11e398ea99c7f7004c2b2e0f580458db42
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@7710ed11e398ea99c7f7004c2b2e0f580458db42
|
|
@ -7,3 +7,9 @@ build
|
|||
specification.json
|
||||
target
|
||||
.DS_Store
|
||||
|
||||
# vscode stuff - we may want to use a more specific pattern later if we'd like to suggest editor configurations
|
||||
.vscode/
|
||||
|
||||
# used for spec compliance tooling
|
||||
java-report.json
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
[submodule "spec"]
|
||||
path = spec
|
||||
url = https://github.com/open-feature/spec/
|
|
@ -0,0 +1,19 @@
|
|||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
wrapperVersion=3.3.2
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
|
|
@ -0,0 +1 @@
|
|||
{".":"1.16.0"}
|
|
@ -0,0 +1,5 @@
|
|||
[spec]
|
||||
file_extension=java
|
||||
multiline_regex=@Specification\((?P<innards>.*?)\)\s*$
|
||||
number_subregex=number\s*=\s*['"](.*?)['"]
|
||||
text_subregex=text\s*=\s*['"](.*)['"]
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,6 @@
|
|||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence
|
||||
#
|
||||
# Managed by Peribolos: https://github.com/open-feature/community/blob/main/config/open-feature/sdk-java/workgroup.yaml
|
||||
#
|
||||
* @open-feature/sdk-java-maintainers @open-feature/maintainers
|
|
@ -1,4 +1,4 @@
|
|||
Welcome! Super happy to have you here.
|
||||
## Welcome! Super happy to have you here.
|
||||
|
||||
A few things.
|
||||
|
||||
|
@ -8,12 +8,75 @@ be a jerk.
|
|||
|
||||
We're not keen on vendor-specific stuff in this library, but if there are changes that need to happen in the spec to enable vendor-specific stuff in user code or other extension points, check out [the spec](https://github.com/open-feature/spec).
|
||||
|
||||
Any contributions you make are expected to be tested with unit tests. You can validate these work with `gradle test`, or the automation itself will run them for you when you make a PR.
|
||||
Any contributions you make are expected to be tested with unit tests. You can validate these work with `mvn test`.
|
||||
Further, it is recommended to verify code styling and static code analysis with `mvn verify -P !deploy`.
|
||||
Regardless, the automation itself will run them for you when you open a PR.
|
||||
|
||||
Your code is supposed to work with Java 11+.
|
||||
> [!TIP]
|
||||
> For easier usage maven wrapper is available. Example usage: `./mvnw verify`
|
||||
|
||||
Your code is supposed to work with Java 8+.
|
||||
|
||||
If you think we might be out of date with the spec, you can check that by invoking `python spec_finder.py` in the root of the repository. This will validate we have tests defined for all of the specification entries we know about.
|
||||
|
||||
If you're adding tests to cover something in the spec, use the `@Specification` annotation like you see throughout the test suites.
|
||||
|
||||
## Code Styles
|
||||
|
||||
### Overview
|
||||
Our project follows strict code formatting standards to maintain consistency and readability across the codebase. We use [Spotless](https://github.com/diffplug/spotless) integrated with the [Palantir Java Format](https://github.com/palantir/palantir-java-format) for code formatting.
|
||||
|
||||
**Spotless** ensures that all code complies with the formatting rules automatically, reducing style-related issues during code reviews.
|
||||
|
||||
### How to Format Your Code
|
||||
1. **Before Committing Changes:**
|
||||
Run the Spotless plugin to format your code. This will apply the Palantir Java Format style:
|
||||
```bash
|
||||
mvn spotless:apply
|
||||
```
|
||||
|
||||
2. **Verify Formatting:**
|
||||
To check if your code adheres to the style guidelines without making changes:
|
||||
```bash
|
||||
mvn spotless:check
|
||||
```
|
||||
|
||||
- If this command fails, your code does not follow the required formatting. Use `mvn spotless:apply` to fix it.
|
||||
|
||||
### CI/CD Integration
|
||||
Our Continuous Integration (CI) pipeline automatically checks code formatting using the Spotless plugin. Any code that does not pass the `spotless:check` step will cause the build to fail.
|
||||
|
||||
### Best Practices
|
||||
- Regularly run `mvn spotless:apply` during your work to ensure your code remains aligned with the standards.
|
||||
- Configure your IDE (e.g., IntelliJ IDEA or Eclipse) to follow the Palantir Java format guidelines to reduce discrepancies during development.
|
||||
|
||||
### Support
|
||||
If you encounter issues with code formatting, please raise a GitHub issue or contact the maintainers.
|
||||
|
||||
## End-to-End Tests
|
||||
|
||||
The continuous integration runs a set of [gherkin e2e tests](https://github.com/open-feature/spec/blob/main/specification/assets/gherkin/evaluation.feature) using `InMemoryProvider`.
|
||||
|
||||
to run alone:
|
||||
```
|
||||
mvn test -P e2e
|
||||
```
|
||||
|
||||
## Benchmarking
|
||||
|
||||
There is a small JMH benchmark suite for testing allocations that can be run with:
|
||||
|
||||
```sh
|
||||
mvn -P benchmark clean compile test-compile jmh:benchmark -Djmh.f=1 -Djmh.prof='dev.openfeature.sdk.benchmark.AllocationProfiler'
|
||||
```
|
||||
|
||||
If you are concerned about the repercussions of a change on memory usage, run this an compare the results to the committed. `benchmark.txt` file.
|
||||
Note that the ONLY MEANINGFUL RESULTS of this benchmark are the `totalAllocatedBytes` and the `totalAllocatedInstances`.
|
||||
The `run` score, and maven task time are not relevant since this benchmark is purely memory-related and has nothing to do with speed.
|
||||
You can also view the heap breakdown to see which objects are taking up the most memory.
|
||||
|
||||
## Releasing
|
||||
|
||||
See [releasing](./docs/release.md).
|
||||
|
||||
Thanks and looking forward to your issues and pull requests.
|
||||
|
|
485
README.md
485
README.md
|
@ -1,69 +1,68 @@
|
|||
# OpenFeature SDK for Java
|
||||
<!-- markdownlint-disable MD033 -->
|
||||
<!-- x-hide-in-docs-start -->
|
||||
<p align="center">
|
||||
<picture>
|
||||
<source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/white/openfeature-horizontal-white.svg" />
|
||||
<source media="(prefers-color-scheme: light)" srcset="https://raw.githubusercontent.com/open-feature/community/0e23508c163a6a1ac8c0ced3e4bd78faafe627c7/assets/logo/horizontal/black/openfeature-horizontal-black.svg" />
|
||||
<img align="center" alt="OpenFeature Logo">
|
||||
</picture>
|
||||
</p>
|
||||
|
||||
[](https://maven-badges.herokuapp.com/maven-central/dev.openfeature/javasdk)
|
||||
[](https://javadoc.io/doc/dev.openfeature/javasdk)
|
||||
[](https://www.repostatus.org/#wip)
|
||||
[](https://snyk.io/test/github/open-feature/java-sdk)
|
||||
[](https://github.com/open-feature/java-sdk/actions/workflows/merge.yml)
|
||||
[](https://codecov.io/gh/open-feature/java-sdk)
|
||||
[](https://bestpractices.coreinfrastructure.org/projects/6241)
|
||||
<h2 align="center">OpenFeature Java SDK</h2>
|
||||
|
||||
<!-- x-hide-in-docs-end -->
|
||||
<!-- The 'github-badges' class is used in the docs -->
|
||||
<p align="center" class="github-badges">
|
||||
<a href="https://github.com/open-feature/spec/releases/tag/v0.7.0">
|
||||
<img alt="Specification" src="https://img.shields.io/static/v1?label=specification&message=v0.7.0&color=yellow&style=for-the-badge" />
|
||||
</a>
|
||||
<!-- x-release-please-start-version -->
|
||||
|
||||
This is the Java implementation of [OpenFeature](https://openfeature.dev), a vendor-agnostic abstraction library for evaluating feature flags.
|
||||
<a href="https://github.com/open-feature/java-sdk/releases/tag/v1.16.0">
|
||||
<img alt="Release" src="https://img.shields.io/static/v1?label=release&message=v1.16.0&color=blue&style=for-the-badge" />
|
||||
</a>
|
||||
|
||||
We support multiple data types for flags (numbers, strings, booleans, objects) as well as hooks, which can alter the lifecycle of a flag evaluation.
|
||||
<!-- x-release-please-end -->
|
||||
<br/>
|
||||
<a href="https://javadoc.io/doc/dev.openfeature/sdk">
|
||||
<img alt="Javadoc" src="https://javadoc.io/badge2/dev.openfeature/sdk/javadoc.svg" />
|
||||
</a>
|
||||
<a href="https://maven-badges.herokuapp.com/maven-central/dev.openfeature/sdk">
|
||||
<img alt="Maven Central" src="https://maven-badges.herokuapp.com/maven-central/dev.openfeature/sdk/badge.svg" />
|
||||
</a>
|
||||
<a href="https://codecov.io/gh/open-feature/java-sdk">
|
||||
<img alt="Codecov" src="https://codecov.io/gh/open-feature/java-sdk/branch/main/graph/badge.svg?token=XMS9L7PBY1" />
|
||||
</a>
|
||||
<a href="https://bestpractices.coreinfrastructure.org/projects/6241">
|
||||
<img alt="CII Best Practices" src="https://bestpractices.coreinfrastructure.org/projects/6241/badge" />
|
||||
</a>
|
||||
</p>
|
||||
<!-- x-hide-in-docs-start -->
|
||||
|
||||
This library is intended to be used in server-side contexts and has not been evaluated for use in mobile devices.
|
||||
[OpenFeature](https://openfeature.dev) is an open specification that provides a vendor-agnostic, community-driven API for feature flagging that works with your favorite feature flag management tool.
|
||||
|
||||
## Usage
|
||||
<!-- x-hide-in-docs-end -->
|
||||
## 🚀 Quick start
|
||||
|
||||
While `Boolean` provides the simplest introduction, we offer a variety of flag types.
|
||||
### Requirements
|
||||
|
||||
```java
|
||||
class MyClass {
|
||||
public UI booleanExample() {
|
||||
// Should we render the redesign? Or the default webpage?
|
||||
if (client.getBooleanValue("redesign_enabled", false)) {
|
||||
return render_redesign();
|
||||
}
|
||||
return render_normal();
|
||||
}
|
||||
|
||||
public Template stringExample() {
|
||||
// Get the template to load for the custom new homepage
|
||||
String template = client.getStringValue("homepage_template", "default-homepage.html")
|
||||
return render_template(template);
|
||||
}
|
||||
|
||||
public List<Module> numberExample() {
|
||||
// How many modules should we be fetching?
|
||||
Integer count = client.getIntegerValue("module-fetch-count", 4);
|
||||
return fetch_modules(count);
|
||||
}
|
||||
|
||||
public Module structureExample() {
|
||||
// This deserializes into the Module structure for you.
|
||||
Module heroModule = client.getObjectValue("hero-module", myExampleModule);
|
||||
return heroModule;
|
||||
}
|
||||
}
|
||||
```
|
||||
- Java 11+ (compiler target is 11)
|
||||
|
||||
## Requirements
|
||||
- Java 8+
|
||||
Note that this library is intended to be used in server-side contexts and has not been evaluated for use on mobile devices.
|
||||
|
||||
## Installation
|
||||
|
||||
### Add it to your build
|
||||
### Install
|
||||
|
||||
#### Maven
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
```xml
|
||||
<dependency>
|
||||
<groupId>dev.openfeature</groupId>
|
||||
<artifactId>javasdk</artifactId>
|
||||
<version>0.0.3-SNAPSHOT</version>
|
||||
<artifactId>sdk</artifactId>
|
||||
<version>1.16.0</version>
|
||||
</dependency>
|
||||
```
|
||||
<!-- x-release-please-end-version -->
|
||||
|
||||
If you would like snapshot builds, this is the relevant repository information:
|
||||
|
||||
|
@ -73,43 +72,397 @@ If you would like snapshot builds, this is the relevant repository information:
|
|||
<snapshots>
|
||||
<enabled>true</enabled>
|
||||
</snapshots>
|
||||
<id>sonartype</id>
|
||||
<name>Sonartype Repository</name>
|
||||
<id>sonatype</id>
|
||||
<name>Sonatype Repository</name>
|
||||
<url>https://s01.oss.sonatype.org/content/repositories/snapshots/</url>
|
||||
</repository>
|
||||
</repositories>
|
||||
```
|
||||
|
||||
#### Gradle
|
||||
|
||||
<!-- x-release-please-start-version -->
|
||||
```groovy
|
||||
dependencies {
|
||||
implementation 'dev.openfeature:javasdk:0.0.3-SNAPSHOT'
|
||||
implementation 'dev.openfeature:sdk:1.16.0'
|
||||
}
|
||||
```
|
||||
<!-- x-release-please-end-version -->
|
||||
|
||||
### Usage
|
||||
|
||||
```java
|
||||
public void example(){
|
||||
|
||||
// flags defined in memory
|
||||
Map<String, Flag<?>> myFlags = new HashMap<>();
|
||||
myFlags.put("v2_enabled", Flag.builder()
|
||||
.variant("on", true)
|
||||
.variant("off", false)
|
||||
.defaultVariant("on")
|
||||
.build());
|
||||
|
||||
// configure a provider
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
try {
|
||||
api.setProviderAndWait(new InMemoryProvider(myFlags));
|
||||
} catch (Exception e) {
|
||||
// handle initialization failure
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
// create a client
|
||||
Client client = api.getClient();
|
||||
|
||||
// get a bool flag value
|
||||
boolean flagValue = client.getBooleanValue("v2_enabled", false);
|
||||
}
|
||||
```
|
||||
|
||||
### Configure it
|
||||
To configure it, you'll need to add a provider to the global singleton `OpenFeatureAPI`. From there, you can generate a `Client` which is usable by your code. While you'll likely want a provider for your specific backend, we've provided a `NoOpProvider`, which simply returns the default passed in.
|
||||
### API Reference
|
||||
|
||||
See [here](https://javadoc.io/doc/dev.openfeature/sdk/latest/) for the Javadocs.
|
||||
|
||||
## 🌟 Features
|
||||
|
||||
| Status | Features | Description |
|
||||
| ------ |---------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------|
|
||||
| ✅ | [Providers](#providers) | Integrate with a commercial, open source, or in-house feature management tool. |
|
||||
| ✅ | [Targeting](#targeting) | Contextually-aware flag evaluation using [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context). |
|
||||
| ✅ | [Hooks](#hooks) | Add functionality to various stages of the flag evaluation life-cycle. |
|
||||
| ✅ | [Tracking](#tracking) | Associate user actions with feature flag evaluations. |
|
||||
| ✅ | [Logging](#logging) | Integrate with popular logging packages. |
|
||||
| ✅ | [Domains](#domains) | Logically bind clients with providers. |
|
||||
| ✅ | [Eventing](#eventing) | React to state changes in the provider or flag management system. |
|
||||
| ✅ | [Shutdown](#shutdown) | Gracefully clean up a provider during application shutdown. |
|
||||
| ✅ | [Transaction Context Propagation](#transaction-context-propagation) | Set a specific [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context) for a transaction (e.g. an HTTP request or a thread). |
|
||||
| ✅ | [Extending](#extending) | Extend OpenFeature with custom providers and hooks. |
|
||||
|
||||
<sub>Implemented: ✅ | In-progress: ⚠️ | Not implemented yet: ❌</sub>
|
||||
|
||||
### Providers
|
||||
|
||||
[Providers](https://openfeature.dev/docs/reference/concepts/provider) are an abstraction between a flag management system and the OpenFeature SDK.
|
||||
Look [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Provider&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Java) for a complete list of available providers.
|
||||
If the provider you're looking for hasn't been created yet, see the [develop a provider](#develop-a-provider) section to learn how to build it yourself.
|
||||
|
||||
Once you've added a provider as a dependency, it can be registered with OpenFeature like this:
|
||||
|
||||
#### Synchronous
|
||||
|
||||
To register a provider in a blocking manner to ensure it is ready before further actions are taken, you can use the `setProviderAndWait` method as shown below:
|
||||
|
||||
```java
|
||||
class MyApp {
|
||||
public void example(){
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.setProvider(new NoOpProvider());
|
||||
Client client = api.getClient();
|
||||
// Now use your `client` instance to evaluate some feature flags!
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
try {
|
||||
api.setProviderAndWait(new MyProvider());
|
||||
} catch (Exception e) {
|
||||
// handle initialization failure
|
||||
e.printStackTrace();
|
||||
}
|
||||
```
|
||||
|
||||
#### Asynchronous
|
||||
|
||||
To register a provider in a non-blocking manner, you can use the `setProvider` method as shown below:
|
||||
|
||||
```java
|
||||
OpenFeatureAPI.getInstance().setProvider(new MyProvider());
|
||||
```
|
||||
|
||||
In some situations, it may be beneficial to register multiple providers in the same application.
|
||||
This is possible using [domains](#domains), which is covered in more detail below.
|
||||
|
||||
### Targeting
|
||||
|
||||
Sometimes, the value of a flag must consider some dynamic criteria about the application or user, such as the user's location, IP, email address, or the server's location.
|
||||
In OpenFeature, we refer to this as [targeting](https://openfeature.dev/specification/glossary#targeting).
|
||||
If the flag management system you're using supports targeting, you can provide the input data using the [evaluation context](https://openfeature.dev/docs/reference/concepts/evaluation-context).
|
||||
|
||||
```java
|
||||
// set a value to the global context
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
Map<String, Value> apiAttrs = new HashMap<>();
|
||||
apiAttrs.put("region", new Value(System.getEnv("us-east-1")));
|
||||
EvaluationContext apiCtx = new ImmutableContext(apiAttrs);
|
||||
api.setEvaluationContext(apiCtx);
|
||||
|
||||
// set a value to the client context
|
||||
Map<String, Value> clientAttrs = new HashMap<>();
|
||||
clientAttrs.put("region", new Value(System.getEnv("us-east-1")));
|
||||
EvaluationContext clientCtx = new ImmutableContext(clientAttrs);
|
||||
Client client = api.getInstance().getClient();
|
||||
client.setEvaluationContext(clientCtx);
|
||||
|
||||
// set a value to the invocation context
|
||||
Map<String, Value> requestAttrs = new HashMap<>();
|
||||
requestAttrs.put("email", new Value(session.getAttribute("email")));
|
||||
requestAttrs.put("product", new Value("productId"));
|
||||
String targetingKey = session.getId();
|
||||
EvaluationContext reqCtx = new ImmutableContext(targetingKey, requestAttrs);
|
||||
|
||||
boolean flagValue = client.getBooleanValue("some-flag", false, reqCtx);
|
||||
```
|
||||
|
||||
### Hooks
|
||||
|
||||
[Hooks](https://openfeature.dev/docs/reference/concepts/hooks) allow for custom logic to be added at well-defined points of the flag evaluation life-cycle
|
||||
Look [here](https://openfeature.dev/ecosystem?instant_search%5BrefinementList%5D%5Btype%5D%5B0%5D=Hook&instant_search%5BrefinementList%5D%5Btechnology%5D%5B0%5D=Java) for a complete list of available hooks.
|
||||
If the hook you're looking for hasn't been created yet, see the [develop a hook](#develop-a-hook) section to learn how to build it yourself.
|
||||
|
||||
Once you've added a hook as a dependency, it can be registered at the global, client, or flag invocation level.
|
||||
|
||||
```java
|
||||
// add a hook globally, to run on all evaluations
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.addHooks(new ExampleHook());
|
||||
|
||||
// add a hook on this client, to run on all evaluations made by this client
|
||||
Client client = api.getClient();
|
||||
client.addHooks(new ExampleHook());
|
||||
|
||||
// add a hook for this evaluation only
|
||||
Boolean retval = client.getBooleanValue(flagKey, false, null,
|
||||
FlagEvaluationOptions.builder().hook(new ExampleHook()).build());
|
||||
```
|
||||
|
||||
### Tracking
|
||||
|
||||
The [tracking API](https://openfeature.dev/specification/sections/tracking/) allows you to use OpenFeature abstractions to associate user actions with feature flag evaluations.
|
||||
This is essential for robust experimentation powered by feature flags. Note that, unlike methods that handle feature flag evaluations, calling `track(...)` may throw an `IllegalArgumentException` if an empty string is passed as the `trackingEventName`.
|
||||
|
||||
```java
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
api.getClient().track("visited-promo-page", new MutableTrackingEventDetails(99.77).add("currency", "USD"));
|
||||
```
|
||||
|
||||
### Logging
|
||||
|
||||
The Java SDK uses SLF4J. See the [SLF4J manual](https://slf4j.org/manual.html) for complete documentation.
|
||||
Note that in accordance with the OpenFeature specification, the SDK doesn't generally log messages during flag evaluation.
|
||||
|
||||
#### Logging Hook
|
||||
|
||||
The Java SDK includes a `LoggingHook`, which logs detailed information at key points during flag evaluation, using SLF4J's structured logging API.
|
||||
This hook can be particularly helpful for troubleshooting and debugging; simply attach it at the global, client or invocation level and ensure your log level is set to "debug".
|
||||
|
||||
See [hooks](#hooks) for more information on configuring hooks.
|
||||
|
||||
### Domains
|
||||
|
||||
Clients can be assigned to a domain.
|
||||
A domain is a logical identifier which can be used to associate clients with a particular provider.
|
||||
If a domain has no associated provider, the global provider is used.
|
||||
|
||||
```java
|
||||
FeatureProvider scopedProvider = new MyProvider();
|
||||
|
||||
// registering the default provider
|
||||
OpenFeatureAPI.getInstance().setProvider(LocalProvider());
|
||||
// registering a provider to a domain
|
||||
OpenFeatureAPI.getInstance().setProvider("my-domain", new CachedProvider());
|
||||
|
||||
// A client bound to the default provider
|
||||
Client clientDefault = OpenFeatureAPI.getInstance().getClient();
|
||||
// A client bound to the CachedProvider provider
|
||||
Client domainScopedClient = OpenFeatureAPI.getInstance().getClient("my-domain");
|
||||
```
|
||||
|
||||
Providers for domains can be set in a blocking or non-blocking way.
|
||||
For more details, please refer to the [providers](#providers) section.
|
||||
|
||||
### Eventing
|
||||
|
||||
Events allow you to react to state changes in the provider or underlying flag management system, such as flag definition changes, provider readiness, or error conditions.
|
||||
Initialization events (`PROVIDER_READY` on success, `PROVIDER_ERROR` on failure) are dispatched for every provider.
|
||||
Some providers support additional events, such as `PROVIDER_CONFIGURATION_CHANGED`.
|
||||
|
||||
Please refer to the documentation of the provider you're using to see what events are supported.
|
||||
|
||||
```java
|
||||
// add an event handler to a client
|
||||
Client client = OpenFeatureAPI.getInstance().getClient();
|
||||
client.onProviderConfigurationChanged((EventDetails eventDetails) -> {
|
||||
// do something when the provider's flag settings change
|
||||
});
|
||||
|
||||
// add an event handler to the global API
|
||||
OpenFeatureAPI.getInstance().onProviderStale((EventDetails eventDetails) -> {
|
||||
// do something when the provider's cache goes stale
|
||||
});
|
||||
```
|
||||
|
||||
### Shutdown
|
||||
|
||||
The OpenFeature API provides a close function to perform a cleanup of all registered providers.
|
||||
This should only be called when your application is in the process of shutting down.
|
||||
|
||||
```java
|
||||
// shut down all providers
|
||||
OpenFeatureAPI.getInstance().shutdown();
|
||||
```
|
||||
|
||||
### Transaction Context Propagation
|
||||
Transaction context is a container for transaction-specific evaluation context (e.g. user id, user agent, IP).
|
||||
Transaction context can be set where specific data is available (e.g. an auth service or request handler) and by using the transaction context propagator it will automatically be applied to all flag evaluations within a transaction (e.g. a request or thread).
|
||||
By default, the `NoOpTransactionContextPropagator` is used, which doesn't store anything.
|
||||
To register a `ThreadLocal` context propagator, you can use the `setTransactionContextPropagator` method as shown below.
|
||||
```java
|
||||
// registering the ThreadLocalTransactionContextPropagator
|
||||
OpenFeatureAPI.getInstance().setTransactionContextPropagator(new ThreadLocalTransactionContextPropagator());
|
||||
```
|
||||
Once you've registered a transaction context propagator, you can propagate the data into request-scoped transaction context.
|
||||
|
||||
```java
|
||||
// adding userId to transaction context
|
||||
OpenFeatureAPI api = OpenFeatureAPI.getInstance();
|
||||
Map<String, Value> transactionAttrs = new HashMap<>();
|
||||
transactionAttrs.put("userId", new Value("userId"));
|
||||
EvaluationContext transactionCtx = new ImmutableContext(transactionAttrs);
|
||||
api.setTransactionContext(transactionCtx);
|
||||
```
|
||||
Additionally, you can develop a custom transaction context propagator by implementing the `TransactionContextPropagator` interface and registering it as shown above.
|
||||
|
||||
## Extending
|
||||
|
||||
### Develop a provider
|
||||
|
||||
To develop a provider, you need to create a new project and include the OpenFeature SDK as a dependency.
|
||||
This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/java-sdk-contrib) available under the OpenFeature organization.
|
||||
You’ll then need to write the provider by implementing the `FeatureProvider` interface exported by the OpenFeature SDK.
|
||||
|
||||
```java
|
||||
public class MyProvider implements FeatureProvider {
|
||||
@Override
|
||||
public Metadata getMetadata() {
|
||||
return () -> "My Provider";
|
||||
}
|
||||
|
||||
@Override
|
||||
public void initialize(EvaluationContext evaluationContext) throws Exception {
|
||||
// start up your provider
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// shut down your provider
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
|
||||
// resolve a boolean flag value
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
|
||||
// resolve a string flag value
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
|
||||
// resolve an int flag value
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
|
||||
// resolve a double flag value
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx) {
|
||||
// resolve an object flag value
|
||||
}
|
||||
}
|
||||
```
|
||||
## Contacting us
|
||||
We hold regular meetings which you can see [here](https://github.com/open-feature/community/#meetings-and-events).
|
||||
|
||||
We are also present on the `#openfeature` channel in the [CNCF slack](https://slack.cncf.io/).
|
||||
If you'd like your provider to support firing events, such as events for when flags are changed in the flag management system, extend `EventProvider`.
|
||||
|
||||
## Contributors
|
||||
```java
|
||||
class MyEventProvider extends EventProvider {
|
||||
@Override
|
||||
public Metadata getMetadata() {
|
||||
return () -> "My Event Provider";
|
||||
}
|
||||
|
||||
Thanks so much to our contributors.
|
||||
@Override
|
||||
public void initialize(EvaluationContext evaluationContext) throws Exception {
|
||||
// emit events when flags are changed in a hypothetical REST API
|
||||
this.restApiClient.onFlagsChanged(() -> {
|
||||
ProviderEventDetails details = ProviderEventDetails.builder().message("flags changed in API!").build();
|
||||
this.emitProviderConfigurationChanged(details);
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
// shut down your provider
|
||||
}
|
||||
|
||||
// remaining provider methods...
|
||||
}
|
||||
```
|
||||
|
||||
Providers no longer need to manage their own state, this is done by the SDK itself. If desired, the state of a provider
|
||||
can be queried through the client that uses the provider.
|
||||
|
||||
```java
|
||||
OpenFeatureAPI.getInstance().getClient().getProviderState();
|
||||
```
|
||||
|
||||
> Built a new provider? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=provider&projects=&template=document-provider.yaml&title=%5BProvider%5D%3A+) so we can add it to the docs!
|
||||
|
||||
### Develop a hook
|
||||
|
||||
To develop a hook, you need to create a new project and include the OpenFeature SDK as a dependency.
|
||||
This can be a new repository or included in [the existing contrib repository](https://github.com/open-feature/java-sdk-contrib) available under the OpenFeature organization.
|
||||
Implement your own hook by conforming to the `Hook interface`.
|
||||
|
||||
```java
|
||||
class MyHook implements Hook {
|
||||
|
||||
@Override
|
||||
public Optional before(HookContext ctx, Map hints) {
|
||||
// code that runs before the flag evaluation
|
||||
}
|
||||
|
||||
@Override
|
||||
public void after(HookContext ctx, FlagEvaluationDetails details, Map hints) {
|
||||
// code that runs after the flag evaluation succeeds
|
||||
}
|
||||
|
||||
@Override
|
||||
public void error(HookContext ctx, Exception error, Map hints) {
|
||||
// code that runs when there's an error during a flag evaluation
|
||||
}
|
||||
|
||||
@Override
|
||||
public void finallyAfter(HookContext ctx, FlagEvaluationDetails details, Map hints) {
|
||||
// code that runs regardless of success or error
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
> Built a new hook? [Let us know](https://github.com/open-feature/openfeature.dev/issues/new?assignees=&labels=hook&projects=&template=document-hook.yaml&title=%5BHook%5D%3A+) so we can add it to the docs!
|
||||
|
||||
<!-- x-hide-in-docs-start -->
|
||||
## ⭐️ Support the project
|
||||
|
||||
- Give this repo a ⭐️!
|
||||
- Follow us on social media:
|
||||
- Twitter: [@openfeature](https://twitter.com/openfeature)
|
||||
- LinkedIn: [OpenFeature](https://www.linkedin.com/company/openfeature/)
|
||||
- Join us on [Slack](https://cloud-native.slack.com/archives/C0344AANLA1)
|
||||
- For more, check out our [community page](https://openfeature.dev/community/)
|
||||
|
||||
## 🤝 Contributing
|
||||
|
||||
Interested in contributing? Great, we'd love your help! To get started, take a look at the [CONTRIBUTING](CONTRIBUTING.md) guide.
|
||||
|
||||
### Thanks to everyone who has already contributed
|
||||
|
||||
<a href="https://github.com/open-feature/java-sdk/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=open-feature/java-sdk" />
|
||||
<img src="https://contrib.rocks/image?repo=open-feature/java-sdk" alt="Pictures of the folks who have contributed to the project" />
|
||||
</a>
|
||||
|
||||
Made with [contrib.rocks](https://contrib.rocks).
|
||||
<!-- x-hide-in-docs-end -->
|
||||
|
|
|
@ -0,0 +1,294 @@
|
|||
[INFO] Scanning for projects...
|
||||
[INFO]
|
||||
[INFO] ------------------------< dev.openfeature:sdk >-------------------------
|
||||
[INFO] Building OpenFeature Java SDK 1.12.1
|
||||
[INFO] from pom.xml
|
||||
[INFO] --------------------------------[ jar ]---------------------------------
|
||||
[WARNING] Parameter 'encoding' is unknown for plugin 'maven-checkstyle-plugin:3.5.0:check (validate)'
|
||||
[WARNING] Parameter 'encoding' is unknown for plugin 'maven-checkstyle-plugin:3.5.0:check (validate)'
|
||||
[WARNING] Parameter 'encoding' is unknown for plugin 'maven-checkstyle-plugin:3.5.0:check (validate)'
|
||||
[INFO]
|
||||
[INFO] --- clean:3.2.0:clean (default-clean) @ sdk ---
|
||||
[INFO] Deleting /home/todd/git/java-sdk/target
|
||||
[INFO]
|
||||
[INFO] --- checkstyle:3.5.0:check (validate) @ sdk ---
|
||||
[INFO] Starting audit...
|
||||
Audit done.
|
||||
[INFO] You have 0 Checkstyle violations.
|
||||
[INFO]
|
||||
[INFO] --- jacoco:0.8.12:prepare-agent (prepare-agent) @ sdk ---
|
||||
[INFO] surefireArgLine set to -javaagent:/home/todd/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/home/todd/git/java-sdk/target/coverage-reports/jacoco-ut.exec
|
||||
[INFO]
|
||||
[INFO] --- resources:3.3.1:resources (default-resources) @ sdk ---
|
||||
[INFO] skip non existing resourceDirectory /home/todd/git/java-sdk/src/main/resources
|
||||
[INFO]
|
||||
[INFO] --- compiler:3.13.0:compile (default-compile) @ sdk ---
|
||||
[INFO] Recompiling the module because of changed source code.
|
||||
[INFO] Compiling 65 source files with javac [debug target 1.8] to target/classes
|
||||
[WARNING] bootstrap class path not set in conjunction with -source 8
|
||||
[WARNING] source value 8 is obsolete and will be removed in a future release
|
||||
[WARNING] target value 8 is obsolete and will be removed in a future release
|
||||
[WARNING] To suppress warnings about obsolete options, use -Xlint:-options.
|
||||
[INFO] Annotation processing is enabled because one or more processors were found
|
||||
on the class path. A future release of javac may disable annotation processing
|
||||
unless at least one processor is specified by name (-processor), or a search
|
||||
path is specified (--processor-path, --processor-module-path), or annotation
|
||||
processing is enabled explicitly (-proc:only, -proc:full).
|
||||
Use -Xlint:-options to suppress this message.
|
||||
Use -proc:none to disable annotation processing.
|
||||
[WARNING] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/MutableStructure.java:[19,1] Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.
|
||||
[WARNING] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/ImmutableStructure.java:[22,1] Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.
|
||||
[WARNING] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/EventDetails.java:[9,1] Generating equals/hashCode implementation but without a call to superclass, even though this class does not extend java.lang.Object. If this is intentional, add '@EqualsAndHashCode(callSuper=false)' to your type.
|
||||
[WARNING] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/Value.java:[27,26] finalize() in java.lang.Object has been deprecated and marked for removal
|
||||
[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/NoOpProvider.java: Some input files use or override a deprecated API.
|
||||
[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/NoOpProvider.java: Recompile with -Xlint:deprecation for details.
|
||||
[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/Value.java: Some input files use unchecked or unsafe operations.
|
||||
[INFO] /home/todd/git/java-sdk/src/main/java/dev/openfeature/sdk/Value.java: Recompile with -Xlint:unchecked for details.
|
||||
[INFO]
|
||||
[INFO] --- checkstyle:3.5.0:check (validate) @ sdk ---
|
||||
[INFO] Starting audit...
|
||||
Audit done.
|
||||
[INFO] You have 0 Checkstyle violations.
|
||||
[INFO]
|
||||
[INFO] --- jacoco:0.8.12:prepare-agent (prepare-agent) @ sdk ---
|
||||
[INFO] surefireArgLine set to -javaagent:/home/todd/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/home/todd/git/java-sdk/target/coverage-reports/jacoco-ut.exec
|
||||
[INFO]
|
||||
[INFO] --- resources:3.3.1:resources (default-resources) @ sdk ---
|
||||
[INFO] skip non existing resourceDirectory /home/todd/git/java-sdk/src/main/resources
|
||||
[INFO]
|
||||
[INFO] --- compiler:3.13.0:compile (default-compile) @ sdk ---
|
||||
[INFO] Nothing to compile - all classes are up to date.
|
||||
[INFO]
|
||||
[INFO] --- resources:3.3.1:testResources (default-testResources) @ sdk ---
|
||||
[INFO] Copying 2 resources from src/test/resources to target/test-classes
|
||||
[INFO]
|
||||
[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ sdk ---
|
||||
[INFO] Recompiling the module because of changed dependency.
|
||||
[INFO] Compiling 52 source files with javac [debug target 1.8] to target/test-classes
|
||||
[WARNING] bootstrap class path not set in conjunction with -source 8
|
||||
[WARNING] source value 8 is obsolete and will be removed in a future release
|
||||
[WARNING] target value 8 is obsolete and will be removed in a future release
|
||||
[WARNING] To suppress warnings about obsolete options, use -Xlint:-options.
|
||||
[INFO] Annotation processing is enabled because one or more processors were found
|
||||
on the class path. A future release of javac may disable annotation processing
|
||||
unless at least one processor is specified by name (-processor), or a search
|
||||
path is specified (--processor-path, --processor-module-path), or annotation
|
||||
processing is enabled explicitly (-proc:only, -proc:full).
|
||||
Use -Xlint:-options to suppress this message.
|
||||
Use -proc:none to disable annotation processing.
|
||||
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/EventsTest.java: Some input files use or override a deprecated API.
|
||||
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/EventsTest.java: Recompile with -Xlint:deprecation for details.
|
||||
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/HookSpecTest.java: Some input files use unchecked or unsafe operations.
|
||||
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/HookSpecTest.java: Recompile with -Xlint:unchecked for details.
|
||||
[INFO]
|
||||
[INFO] >>> jmh:0.2.2:benchmark (default-cli) > process-test-resources @ sdk >>>
|
||||
[INFO]
|
||||
[INFO] --- checkstyle:3.5.0:check (validate) @ sdk ---
|
||||
[INFO] Starting audit...
|
||||
Audit done.
|
||||
[INFO] You have 0 Checkstyle violations.
|
||||
[INFO]
|
||||
[INFO] --- jacoco:0.8.12:prepare-agent (prepare-agent) @ sdk ---
|
||||
[INFO] surefireArgLine set to -javaagent:/home/todd/.m2/repository/org/jacoco/org.jacoco.agent/0.8.12/org.jacoco.agent-0.8.12-runtime.jar=destfile=/home/todd/git/java-sdk/target/coverage-reports/jacoco-ut.exec
|
||||
[INFO]
|
||||
[INFO] --- resources:3.3.1:resources (default-resources) @ sdk ---
|
||||
[INFO] skip non existing resourceDirectory /home/todd/git/java-sdk/src/main/resources
|
||||
[INFO]
|
||||
[INFO] --- compiler:3.13.0:compile (default-compile) @ sdk ---
|
||||
[INFO] Nothing to compile - all classes are up to date.
|
||||
[INFO]
|
||||
[INFO] --- resources:3.3.1:testResources (default-testResources) @ sdk ---
|
||||
[INFO] Copying 2 resources from src/test/resources to target/test-classes
|
||||
[INFO]
|
||||
[INFO] <<< jmh:0.2.2:benchmark (default-cli) < process-test-resources @ sdk <<<
|
||||
[INFO]
|
||||
[INFO]
|
||||
[INFO] --- jmh:0.2.2:benchmark (default-cli) @ sdk ---
|
||||
[INFO] Changes detected - recompiling the module!
|
||||
[INFO] Compiling 52 source files to /home/todd/git/java-sdk/target/test-classes
|
||||
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/LockingTest.java: Some input files use or override a deprecated API.
|
||||
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/LockingTest.java: Recompile with -Xlint:deprecation for details.
|
||||
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/internal/TriConsumerTest.java: Some input files use unchecked or unsafe operations.
|
||||
[INFO] /home/todd/git/java-sdk/src/test/java/dev/openfeature/sdk/internal/TriConsumerTest.java: Recompile with -Xlint:unchecked for details.
|
||||
[INFO] Executing the JMH benchmarks
|
||||
# JMH version: 1.37
|
||||
# VM version: JDK 21.0.4, OpenJDK 64-Bit Server VM, 21.0.4+7
|
||||
# VM invoker: /usr/lib/jvm/java-21-openjdk/bin/java
|
||||
# VM options: -Xmx1024m -XX:+UnlockExperimentalVMOptions -XX:+UseEpsilonGC
|
||||
# Blackhole mode: compiler (auto-detected, use -Djmh.blackhole.autoDetect=false to disable)
|
||||
# Warmup: <none>
|
||||
# Measurement: 1 iterations, single-shot each
|
||||
# Timeout: 10 min per iteration
|
||||
# Threads: 1 thread
|
||||
# Benchmark mode: Single shot invocation time
|
||||
# Benchmark: dev.openfeature.sdk.benchmark.AllocationBenchmark.run
|
||||
|
||||
# Run progress: 0.00% complete, ETA 00:00:00
|
||||
# Fork: 1 of 1
|
||||
[0.001s][warning][gc,init] Consider setting -Xms equal to -Xmx to avoid resizing hiccups
|
||||
[0.001s][warning][gc,init] Consider enabling -XX:+AlwaysPreTouch to avoid memory commit hiccups
|
||||
Iteration 1: num #instances #bytes class name (module)
|
||||
-------------------------------------------------------
|
||||
1: 480234 23051232 java.util.HashMap (java.base@21.0.4)
|
||||
2: 150497 12050088 [Ljava.util.HashMap$Node; (java.base@21.0.4)
|
||||
3: 332017 10624544 java.util.HashMap$Node (java.base@21.0.4)
|
||||
4: 47815 9732480 [B (java.base@21.0.4)
|
||||
5: 305991 8105872 [Ljava.lang.Object; (java.base@21.0.4)
|
||||
6: 366682 5866912 java.util.Optional (java.base@21.0.4)
|
||||
7: 183332 5866624 java.util.HashMap$EntryIterator (java.base@21.0.4)
|
||||
8: 172970 5535040 java.util.Collections$UnmodifiableMap (java.base@21.0.4)
|
||||
9: 100000 4000000 dev.openfeature.sdk.HookContext
|
||||
10: 100000 4000000 dev.openfeature.sdk.HookContext$HookContextBuilder
|
||||
11: 230006 3680096 dev.openfeature.sdk.Value
|
||||
12: 200062 3200992 java.util.HashMap$EntrySet (java.base@21.0.4)
|
||||
13: 132870 3188880 java.util.ArrayList (java.base@21.0.4)
|
||||
14: 192292 3076672 dev.openfeature.sdk.ImmutableStructure
|
||||
15: 182292 2916672 dev.openfeature.sdk.ImmutableContext
|
||||
16: 50000 2000000 dev.openfeature.sdk.FlagEvaluationDetails
|
||||
17: 50000 2000000 dev.openfeature.sdk.ProviderEvaluation
|
||||
18: 122968 1967488 java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet (java.base@21.0.4)
|
||||
19: 149 1884376 [Ljdk.internal.vm.FillerElement; (java.base@21.0.4)
|
||||
20: 56476 1807232 java.util.ArrayList$Itr (java.base@21.0.4)
|
||||
21: 37481 1799088 dev.openfeature.sdk.FlagEvaluationDetails$FlagEvaluationDetailsBuilder
|
||||
22: 100001 1600016 dev.openfeature.sdk.NoOpProvider$$Lambda/0x000076e79c02fa78
|
||||
23: 50000 1600000 [Ldev.openfeature.sdk.EvaluationContext;
|
||||
24: 50000 1600000 [Ljava.util.List; (java.base@21.0.4)
|
||||
25: 100000 1600000 dev.openfeature.sdk.OpenFeatureClient$$Lambda/0x000076e79c082800
|
||||
26: 36720 1468800 dev.openfeature.sdk.ProviderEvaluation$ProviderEvaluationBuilder
|
||||
27: 87481 1399696 dev.openfeature.sdk.ImmutableMetadata
|
||||
28: 50000 1200000 dev.openfeature.sdk.FlagEvaluationOptions
|
||||
29: 74201 1187216 dev.openfeature.sdk.ImmutableMetadata$ImmutableMetadataBuilder
|
||||
30: 73235 1171760 java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$UnmodifiableEntry (java.base@21.0.4)
|
||||
31: 45869 1100856 java.util.Collections$UnmodifiableMap$UnmodifiableEntrySet$1 (java.base@21.0.4)
|
||||
32: 43776 1050624 dev.openfeature.sdk.FlagEvaluationOptions$FlagEvaluationOptionsBuilder
|
||||
33: 40016 960384 dev.openfeature.sdk.HookSupport$$Lambda/0x000076e79c081b78
|
||||
34: 39967 959208 dev.openfeature.sdk.HookSupport$$Lambda/0x000076e79c081da8
|
||||
35: 57783 924528 dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock$$Lambda/0x000076e79c02eae8
|
||||
36: 4490 721440 [I (java.base@21.0.4)
|
||||
37: 26594 638256 java.lang.String (java.base@21.0.4)
|
||||
38: 1461 390008 [J (java.base@21.0.4)
|
||||
39: 2361 288784 java.lang.Class (java.base@21.0.4)
|
||||
40: 4632 259392 jdk.internal.org.objectweb.asm.SymbolTable$Entry (java.base@21.0.4)
|
||||
41: 10001 240024 java.lang.Double (java.base@21.0.4)
|
||||
42: 2502 180144 java.lang.reflect.Field (java.base@21.0.4)
|
||||
43: 6007 144168 java.lang.StringBuilder (java.base@21.0.4)
|
||||
44: 180 140968 [Ljdk.internal.org.objectweb.asm.SymbolTable$Entry; (java.base@21.0.4)
|
||||
45: 3827 122464 java.util.concurrent.ConcurrentHashMap$Node (java.base@21.0.4)
|
||||
46: 48 122168 [C (java.base@21.0.4)
|
||||
47: 1440 113512 [S (java.base@21.0.4)
|
||||
48: 1201 105688 java.lang.reflect.Method (java.base@21.0.4)
|
||||
49: 3031 79600 [Ljava.lang.Class; (java.base@21.0.4)
|
||||
50: 1351 75656 jdk.internal.org.objectweb.asm.Label (java.base@21.0.4)
|
||||
51: 1561 74928 java.lang.invoke.MemberName (java.base@21.0.4)
|
||||
52: 334 74816 jdk.internal.org.objectweb.asm.MethodWriter (java.base@21.0.4)
|
||||
53: 1799 71960 java.lang.invoke.MethodType (java.base@21.0.4)
|
||||
54: 1089 69696 java.net.URL (java.base@21.0.4)
|
||||
55: 121 50512 [Ljava.util.concurrent.ConcurrentHashMap$Node; (java.base@21.0.4)
|
||||
56: 3147 50352 jdk.internal.util.StrongReferenceKey (java.base@21.0.4)
|
||||
57: 1057 42280 java.io.ObjectStreamField (java.base@21.0.4)
|
||||
58: 1225 39200 java.io.File (java.base@21.0.4)
|
||||
59: 779 37392 jdk.internal.org.objectweb.asm.Frame (java.base@21.0.4)
|
||||
60: 243 25272 java.util.jar.JarFile$JarFileEntry (java.base@21.0.4)
|
||||
61: 794 25248 [Ljava.lang.String; (java.base@21.0.4)
|
||||
62: 622 24880 java.lang.NoSuchFieldException (java.base@21.0.4)
|
||||
63: 571 22840 java.util.LinkedHashMap$Entry (java.base@21.0.4)
|
||||
64: 474 22752 jdk.internal.ref.CleanerImpl$PhantomCleanableRef (java.base@21.0.4)
|
||||
65: 690 22080 jdk.internal.util.WeakReferenceKey (java.base@21.0.4)
|
||||
66: 828 19872 jdk.internal.org.objectweb.asm.ByteVector (java.base@21.0.4)
|
||||
67: 248 18848 [Ljava.lang.ref.SoftReference; (java.base@21.0.4)
|
||||
68: 118 17936 jdk.internal.org.objectweb.asm.ClassWriter (java.base@21.0.4)
|
||||
69: 380 16824 [Ljava.lang.invoke.LambdaForm$Name; (java.base@21.0.4)
|
||||
70: 625 15000 java.lang.Long (java.base@21.0.4)
|
||||
71: 463 14816 java.lang.invoke.LambdaForm$Name (java.base@21.0.4)
|
||||
72: 904 14464 java.lang.Object (java.base@21.0.4)
|
||||
73: 198 14256 java.lang.reflect.Constructor (java.base@21.0.4)
|
||||
74: 249 13944 java.util.zip.ZipFile$ZipFileInputStream (java.base@21.0.4)
|
||||
75: 334 13360 jdk.internal.org.objectweb.asm.Handler (java.base@21.0.4)
|
||||
76: 202 12928 java.util.concurrent.ConcurrentHashMap (java.base@21.0.4)
|
||||
77: 201 12864 jdk.internal.org.objectweb.asm.FieldWriter (java.base@21.0.4)
|
||||
78: 316 12640 java.util.WeakHashMap$Entry (java.base@21.0.4)
|
||||
79: 102 12240 java.io.ObjectStreamClass (java.base@21.0.4)
|
||||
80: 249 11952 java.util.zip.ZipFile$ZipFileInflaterInputStream (java.base@21.0.4)
|
||||
81: 359 11488 jdk.internal.org.objectweb.asm.Type (java.base@21.0.4)
|
||||
82: 465 11160 java.lang.invoke.ResolvedMethodName (java.base@21.0.4)
|
||||
83: 464 11136 jdk.internal.org.objectweb.asm.Edge (java.base@21.0.4)
|
||||
84: 341 10912 jdk.internal.math.FDBigInteger (java.base@21.0.4)
|
||||
85: 94 10728 [Ljava.lang.reflect.Field; (java.base@21.0.4)
|
||||
86: 266 10640 java.lang.NoSuchMethodException (java.base@21.0.4)
|
||||
87: 266 10640 java.security.CodeSource (java.base@21.0.4)
|
||||
88: 221 10608 java.lang.invoke.DirectMethodHandle$Constructor (java.base@21.0.4)
|
||||
89: 264 10560 sun.security.util.KnownOIDs (java.base@21.0.4)
|
||||
90: 75 10200 sun.nio.fs.UnixFileAttributes (java.base@21.0.4)
|
||||
91: 245 9800 java.lang.ref.SoftReference (java.base@21.0.4)
|
||||
92: 118 9440 jdk.internal.event.DeserializationEvent (java.base@21.0.4)
|
||||
93: 115 9200 [Ljava.util.WeakHashMap$Entry; (java.base@21.0.4)
|
||||
94: 368 8832 java.lang.module.ModuleDescriptor$Exports (java.base@21.0.4)
|
||||
95: 63 8384 [Ljava.lang.invoke.MethodHandle; (java.base@21.0.4)
|
||||
96: 146 8176 java.io.FileCleanable (java.base@21.0.4)
|
||||
97: 125 8000 java.lang.Class$ReflectionData (java.base@21.0.4)
|
||||
98: 323 7752 java.util.ImmutableCollections$Set12 (java.base@21.0.4)
|
||||
99: 121 7744 jdk.internal.org.objectweb.asm.SymbolTable (java.base@21.0.4)
|
||||
100: 70 7280 java.lang.invoke.InnerClassLambdaMetafactory (java.base@21.0.4)
|
||||
101: 144 6912 jdk.internal.org.objectweb.asm.AnnotationWriter (java.base@21.0.4)
|
||||
102: 167 6680 jdk.internal.loader.URLClassPath$JarLoader$2 (java.base@21.0.4)
|
||||
103: 199 6368 java.lang.invoke.MethodHandles$Lookup (java.base@21.0.4)
|
||||
104: 156 6240 java.util.StringJoiner (java.base@21.0.4)
|
||||
105: 153 6120 java.io.FileDescriptor (java.base@21.0.4)
|
||||
106: 126 6048 java.lang.invoke.LambdaForm (java.base@21.0.4)
|
||||
107: 77 6016 [Ljava.lang.reflect.Method; (java.base@21.0.4)
|
||||
108: 249 5976 java.util.zip.ZipFile$InflaterCleanupAction (java.base@21.0.4)
|
||||
109: 373 5968 java.lang.Byte (java.base@21.0.4)
|
||||
110: 74 5920 java.util.zip.ZipFile$Source (java.base@21.0.4)
|
||||
111: 82 5720 [Ljava.io.ObjectStreamField; (java.base@21.0.4)
|
||||
112: 40 5640 [Ljava.lang.ClassValue$Entry; (java.base@21.0.4)
|
||||
113: 234 5616 java.util.jar.Attributes$Name (java.base@21.0.4)
|
||||
114: 174 5568 java.util.concurrent.locks.ReentrantLock$NonfairSync (java.base@21.0.4)
|
||||
115: 98 5488 java.lang.Module (java.base@21.0.4)
|
||||
116: 219 5256 java.lang.PublicMethods$MethodList (java.base@21.0.4)
|
||||
117: 65 5200 java.net.URI (java.base@21.0.4)
|
||||
118: 215 5104 [Ljdk.internal.org.objectweb.asm.Type; (java.base@21.0.4)
|
||||
truncated...
|
||||
Total 4452140 139359040
|
||||
|
||||
0.186 s/op
|
||||
+totalAllocatedBytes: 139359040.000 bytes
|
||||
+totalAllocatedInstances: 4452140.000 instances
|
||||
+totalHeap: 521412608.000 bytes
|
||||
|
||||
|
||||
|
||||
Secondary result "dev.openfeature.sdk.benchmark.AllocationBenchmark.run:+totalAllocatedBytes":
|
||||
139359040.000 bytes
|
||||
|
||||
Secondary result "dev.openfeature.sdk.benchmark.AllocationBenchmark.run:+totalAllocatedInstances":
|
||||
4452140.000 instances
|
||||
|
||||
Secondary result "dev.openfeature.sdk.benchmark.AllocationBenchmark.run:+totalHeap":
|
||||
521412608.000 bytes
|
||||
|
||||
|
||||
# Run complete. Total time: 00:00:00
|
||||
|
||||
REMEMBER: The numbers below are just data. To gain reusable insights, you need to follow up on
|
||||
why the numbers are the way they are. Use profilers (see -prof, -lprof), design factorial
|
||||
experiments, perform baseline and negative tests that provide experimental control, make sure
|
||||
the benchmarking environment is safe on JVM/OS/HW level, ask for reviews from the domain experts.
|
||||
Do not assume the numbers tell you what you want them to tell.
|
||||
|
||||
NOTE: Current JVM experimentally supports Compiler Blackholes, and they are in use. Please exercise
|
||||
extra caution when trusting the results, look into the generated code to check the benchmark still
|
||||
works, and factor in a small probability of new VM bugs. Additionally, while comparisons between
|
||||
different JVMs are already problematic, the performance difference caused by different Blackhole
|
||||
modes can be very significant. Please make sure you use the consistent Blackhole mode for comparisons.
|
||||
|
||||
Benchmark Mode Cnt Score Error Units
|
||||
AllocationBenchmark.run ss 0.186 s/op
|
||||
AllocationBenchmark.run:+totalAllocatedBytes ss 139359040.000 bytes
|
||||
AllocationBenchmark.run:+totalAllocatedInstances ss 4452140.000 instances
|
||||
AllocationBenchmark.run:+totalHeap ss 521412608.000 bytes
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[INFO] Total time: 8.280 s
|
||||
[INFO] Finished at: 2024-10-23T12:37:24-04:00
|
||||
[INFO] ------------------------------------------------------------------------
|
|
@ -0,0 +1,7 @@
|
|||
<?xml version="1.0"?>
|
||||
|
||||
<!DOCTYPE suppressions PUBLIC "-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN" "https://checkstyle.org/dtds/suppressions_1_2.dtd">
|
||||
|
||||
<suppressions>
|
||||
<!-- checkstyle suppressions can go here -->
|
||||
</suppressions>
|
282
checkstyle.xml
282
checkstyle.xml
|
@ -1,7 +1,7 @@
|
|||
<?xml version="1.0"?>
|
||||
<!DOCTYPE module PUBLIC
|
||||
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
|
||||
"https://checkstyle.org/dtds/configuration_1_3.dtd">
|
||||
|
||||
<!--
|
||||
Checkstyle configuration that checks the Google coding conventions from Google Java Style
|
||||
|
@ -13,17 +13,18 @@
|
|||
To completely disable a check, just comment it out or delete it from the file.
|
||||
To suppress certain violations please review suppression filters.
|
||||
|
||||
Authors: Max Vetrenko, Ruslan Diachenko, Roman Ivanov.
|
||||
Authors: Max Vetrenko, Mauryan Kansara, Ruslan Diachenko, Roman Ivanov.
|
||||
-->
|
||||
|
||||
<module name = "Checker">
|
||||
<module name="Checker">
|
||||
|
||||
<property name="charset" value="UTF-8"/>
|
||||
|
||||
<property name="severity" value="error"/>
|
||||
|
||||
<property name="fileExtensions" value="java, properties, xml"/>
|
||||
<!-- Excludes all 'module-info.java' files -->
|
||||
<!-- See https://checkstyle.org/config_filefilters.html -->
|
||||
<!-- See https://checkstyle.org/filefilters/index.html -->
|
||||
<module name="BeforeExecutionExclusionFileFilter">
|
||||
<property name="fileNamePattern" value="module\-info\.java$"/>
|
||||
</module>
|
||||
|
@ -34,8 +35,16 @@
|
|||
<property name="optional" value="true"/>
|
||||
</module>
|
||||
|
||||
<!-- https://checkstyle.org/filters/suppresswithnearbytextfilter.html -->
|
||||
<!--<module name="SuppressWithNearbyTextFilter">
|
||||
<property name="nearbyTextPattern"
|
||||
value="CHECKSTYLE.SUPPRESS\: (\w+) for ([+-]\d+) lines"/>
|
||||
<property name="checkPattern" value="$1"/>
|
||||
<property name="lineRange" value="$2"/>
|
||||
</module>-->
|
||||
|
||||
<!-- Checks for whitespace -->
|
||||
<!-- See http://checkstyle.org/config_whitespace.html -->
|
||||
<!-- See http://checkstyle.org/checks/whitespace/index.html -->
|
||||
<module name="FileTabCharacter">
|
||||
<property name="eachLine" value="true"/>
|
||||
</module>
|
||||
|
@ -46,53 +55,90 @@
|
|||
<property name="ignorePattern" value="^package.*|^import.*|a href|href|http://|https://|ftp://"/>
|
||||
</module>
|
||||
|
||||
<module name="SuppressWarningsFilter" />
|
||||
|
||||
<module name="TreeWalker">
|
||||
<!-- needed for SuppressWarningsFilter -->
|
||||
<module name="SuppressWarningsHolder" />
|
||||
|
||||
<module name="SuppressWarnings">
|
||||
<property name="id" value="checkstyle:suppresswarnings"/>
|
||||
</module>
|
||||
|
||||
<!-- https://checkstyle.org/config_filters.html#SuppressionXpathFilter -->
|
||||
<module name="SuppressionXpathFilter">
|
||||
<property name="file" value="${org.checkstyle.google.suppressionxpathfilter.config}"
|
||||
default="checkstyle-xpath-suppressions.xml" />
|
||||
<property name="optional" value="true"/>
|
||||
</module>
|
||||
|
||||
<module name="UnusedImports"/>
|
||||
<module name="OuterTypeFilename"/>
|
||||
<module name="IllegalTokenText">
|
||||
<property name="tokens" value="STRING_LITERAL, CHAR_LITERAL"/>
|
||||
<property name="format"
|
||||
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
|
||||
value="\\u00(09|0(a|A)|0(c|C)|0(d|D)|22|27|5(C|c))|\\(0(10|11|12|14|15|42|47)|134)"/>
|
||||
<property name="message"
|
||||
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
|
||||
value="Consider using special escape sequence instead of octal value or Unicode escaped value."/>
|
||||
</module>
|
||||
<module name="AvoidEscapedUnicodeCharacters">
|
||||
<property name="allowEscapesForControlCharacters" value="true"/>
|
||||
<property name="allowByTailComment" value="true"/>
|
||||
<property name="allowNonPrintableEscapes" value="true"/>
|
||||
</module>
|
||||
<module name="AvoidStarImport"/>
|
||||
<module name="OneTopLevelClass"/>
|
||||
<module name="NoLineWrap">
|
||||
<property name="tokens" value="PACKAGE_DEF, IMPORT, STATIC_IMPORT"/>
|
||||
</module>
|
||||
<module name="EmptyBlock">
|
||||
<property name="option" value="TEXT"/>
|
||||
<property name="tokens"
|
||||
value="LITERAL_TRY, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE, LITERAL_SWITCH"/>
|
||||
</module>
|
||||
<module name="NeedBraces">
|
||||
<property name="tokens"
|
||||
value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/>
|
||||
value="LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF, LITERAL_WHILE"/>
|
||||
</module>
|
||||
<module name="LeftCurly">
|
||||
<property name="id" value="LeftCurlyEol"/>
|
||||
<property name="tokens"
|
||||
value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,
|
||||
INTERFACE_DEF, LAMBDA, LITERAL_CASE, LITERAL_CATCH, LITERAL_DEFAULT,
|
||||
LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY, LITERAL_FOR, LITERAL_IF,
|
||||
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_TRY, LITERAL_WHILE, METHOD_DEF,
|
||||
OBJBLOCK, STATIC_INIT"/>
|
||||
value="ANNOTATION_DEF, CLASS_DEF, CTOR_DEF, ENUM_CONSTANT_DEF, ENUM_DEF,
|
||||
INTERFACE_DEF, LITERAL_CATCH,
|
||||
LITERAL_DO, LITERAL_ELSE, LITERAL_FOR, LITERAL_IF,
|
||||
LITERAL_WHILE, METHOD_DEF,
|
||||
OBJBLOCK, STATIC_INIT, RECORD_DEF, COMPACT_CTOR_DEF"/>
|
||||
</module>
|
||||
<module name="LeftCurly">
|
||||
<property name="id" value="LeftCurlyNl"/>
|
||||
<property name="option" value="nl"/>
|
||||
<property name="tokens"
|
||||
value=" LITERAL_DEFAULT"/>
|
||||
</module>
|
||||
<module name="SuppressionXpathSingleFilter">
|
||||
<!-- LITERAL_DEFAULT are reused in SWITCH_RULE -->
|
||||
<property name="id" value="LeftCurlyNl"/>
|
||||
<property name="query" value="//SWITCH_RULE/SLIST"/>
|
||||
</module>
|
||||
<module name="RightCurly">
|
||||
<property name="id" value="RightCurlySame"/>
|
||||
<property name="tokens"
|
||||
value="LITERAL_TRY, LITERAL_CATCH, LITERAL_FINALLY, LITERAL_IF, LITERAL_ELSE,
|
||||
value="LITERAL_IF, LITERAL_ELSE,
|
||||
LITERAL_DO"/>
|
||||
</module>
|
||||
<module name="RightCurly">
|
||||
<property name="id" value="RightCurlyAlone"/>
|
||||
<property name="option" value="alone"/>
|
||||
<property name="tokens"
|
||||
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
|
||||
INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF"/>
|
||||
value="CLASS_DEF, METHOD_DEF, CTOR_DEF, LITERAL_FOR, LITERAL_WHILE, STATIC_INIT,
|
||||
INSTANCE_INIT, ANNOTATION_DEF, ENUM_DEF, INTERFACE_DEF, RECORD_DEF,
|
||||
COMPACT_CTOR_DEF"/>
|
||||
</module>
|
||||
<module name="SuppressionXpathSingleFilter">
|
||||
<!-- suppression is required till https://github.com/checkstyle/checkstyle/issues/7541 -->
|
||||
<property name="id" value="RightCurlyAlone"/>
|
||||
<property name="query" value="//RCURLY[parent::SLIST[count(./*)=1]
|
||||
or preceding-sibling::*[last()][self::LCURLY]]"/>
|
||||
</module>
|
||||
<module name="WhitespaceAfter">
|
||||
<property name="tokens"
|
||||
value="COMMA, SEMI, TYPECAST, LITERAL_IF, LITERAL_ELSE,
|
||||
LITERAL_WHILE, LITERAL_DO, LITERAL_FOR, DO_WHILE"/>
|
||||
</module>
|
||||
<module name="WhitespaceAround">
|
||||
<property name="allowEmptyConstructors" value="true"/>
|
||||
|
@ -100,18 +146,35 @@
|
|||
<property name="allowEmptyMethods" value="true"/>
|
||||
<property name="allowEmptyTypes" value="true"/>
|
||||
<property name="allowEmptyLoops" value="true"/>
|
||||
<!--<property name="allowEmptySwitchBlockStatements" value="true"/>-->
|
||||
<property name="ignoreEnhancedForColon" value="false"/>
|
||||
<property name="tokens"
|
||||
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,
|
||||
BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAMBDA, LAND,
|
||||
LCURLY, LE, LITERAL_CATCH, LITERAL_DO, LITERAL_ELSE, LITERAL_FINALLY,
|
||||
LITERAL_FOR, LITERAL_IF, LITERAL_RETURN, LITERAL_SWITCH, LITERAL_SYNCHRONIZED,
|
||||
LITERAL_TRY, LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
|
||||
NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
|
||||
SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT, TYPE_EXTENSION_AND"/>
|
||||
value="ASSIGN, BAND, BAND_ASSIGN, BOR, BOR_ASSIGN, BSR, BSR_ASSIGN, BXOR,
|
||||
BXOR_ASSIGN, COLON, DIV, DIV_ASSIGN, DO_WHILE, EQUAL, GE, GT, LAND,
|
||||
LCURLY, LE, LITERAL_DO, LITERAL_ELSE,
|
||||
LITERAL_FOR, LITERAL_IF,
|
||||
LITERAL_WHILE, LOR, LT, MINUS, MINUS_ASSIGN, MOD, MOD_ASSIGN,
|
||||
NOT_EQUAL, PLUS, PLUS_ASSIGN, QUESTION, RCURLY, SL, SLIST, SL_ASSIGN, SR,
|
||||
SR_ASSIGN, STAR, STAR_ASSIGN, LITERAL_ASSERT,
|
||||
TYPE_EXTENSION_AND"/>
|
||||
<message key="ws.notFollowed"
|
||||
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
|
||||
value="WhitespaceAround: ''{0}'' is not followed by whitespace. Empty blocks
|
||||
may only be represented as '{}' when not part of a multi-block statement (4.1.3)"/>
|
||||
<message key="ws.notPreceded"
|
||||
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
|
||||
value="WhitespaceAround: ''{0}'' is not preceded with whitespace."/>
|
||||
</module>
|
||||
<module name="SuppressionXpathSingleFilter">
|
||||
<property name="checks" value="WhitespaceAround"/>
|
||||
<property name="query" value="//*[self::LITERAL_IF or self::LITERAL_ELSE or self::STATIC_INIT
|
||||
or self::LITERAL_TRY or self::LITERAL_CATCH]/SLIST[count(./*)=1]
|
||||
| //*[self::STATIC_INIT or self::LITERAL_TRY or self::LITERAL_IF]
|
||||
//*[self::RCURLY][parent::SLIST[count(./*)=1]]"/>
|
||||
</module>
|
||||
<module name="RegexpSinglelineJava">
|
||||
<property name="format" value="\{[ ]+\}"/>
|
||||
<property name="message" value="Empty blocks should have no spaces. Empty blocks
|
||||
may only be represented as '{}' when not part of a
|
||||
multi-block statement (4.1.3)"/>
|
||||
</module>
|
||||
<module name="OneStatementPerLine"/>
|
||||
<module name="MultipleVariableDeclarations"/>
|
||||
|
@ -122,8 +185,9 @@
|
|||
<module name="ModifierOrder"/>
|
||||
<module name="EmptyLineSeparator">
|
||||
<property name="tokens"
|
||||
value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
|
||||
STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
|
||||
value="PACKAGE_DEF, IMPORT, STATIC_IMPORT, CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
|
||||
STATIC_INIT, INSTANCE_INIT, METHOD_DEF, CTOR_DEF, VARIABLE_DEF, RECORD_DEF,
|
||||
COMPACT_CTOR_DEF"/>
|
||||
<property name="allowNoEmptyLineBetweenFields" value="true"/>
|
||||
</module>
|
||||
<module name="SeparatorWrap">
|
||||
|
@ -137,13 +201,13 @@
|
|||
<property name="option" value="EOL"/>
|
||||
</module>
|
||||
<module name="SeparatorWrap">
|
||||
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/258 -->
|
||||
<!-- ELLIPSIS is EOL until https://github.com/google/styleguide/issues/259 -->
|
||||
<property name="id" value="SeparatorWrapEllipsis"/>
|
||||
<property name="tokens" value="ELLIPSIS"/>
|
||||
<property name="option" value="EOL"/>
|
||||
</module>
|
||||
<module name="SeparatorWrap">
|
||||
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/259 -->
|
||||
<!-- ARRAY_DECLARATOR is EOL until https://github.com/google/styleguide/issues/258 -->
|
||||
<property name="id" value="SeparatorWrapArrayDeclarator"/>
|
||||
<property name="tokens" value="ARRAY_DECLARATOR"/>
|
||||
<property name="option" value="EOL"/>
|
||||
|
@ -156,22 +220,23 @@
|
|||
<module name="PackageName">
|
||||
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Package name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Package name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="TypeName">
|
||||
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF"/>
|
||||
<property name="tokens" value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
|
||||
ANNOTATION_DEF, RECORD_DEF"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Type name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="MemberName">
|
||||
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Member name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Member name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="ParameterName">
|
||||
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Parameter name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="LambdaParameterName">
|
||||
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
|
||||
|
@ -181,38 +246,53 @@
|
|||
<module name="CatchParameterName">
|
||||
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Catch parameter name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="LocalVariableName">
|
||||
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Local variable name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="PatternVariableName">
|
||||
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Pattern variable name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="ClassTypeParameterName">
|
||||
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Class type name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Class type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="RecordComponentName">
|
||||
<property name="format" value="^[a-z]([a-z0-9][a-zA-Z0-9]*)?$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Record component name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="RecordTypeParameterName">
|
||||
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Record type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="MethodTypeParameterName">
|
||||
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Method type name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Method type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="InterfaceTypeParameterName">
|
||||
<property name="format" value="(^[A-Z][0-9]?)$|([A-Z][a-zA-Z0-9]*[T]$)"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Interface type name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="NoFinalizer"/>
|
||||
<module name="GenericWhitespace">
|
||||
<message key="ws.followed"
|
||||
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
|
||||
value="GenericWhitespace ''{0}'' is followed by whitespace."/>
|
||||
<message key="ws.preceded"
|
||||
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
|
||||
value="GenericWhitespace ''{0}'' is preceded with whitespace."/>
|
||||
<message key="ws.illegalFollow"
|
||||
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
|
||||
value="GenericWhitespace ''{0}'' should followed by whitespace."/>
|
||||
<message key="ws.notPreceded"
|
||||
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
|
||||
value="GenericWhitespace ''{0}'' is not preceded with whitespace."/>
|
||||
</module>
|
||||
<module name="Indentation">
|
||||
<property name="basicOffset" value="4"/>
|
||||
|
@ -222,44 +302,62 @@
|
|||
<property name="lineWrappingIndentation" value="4"/>
|
||||
<property name="arrayInitIndent" value="4"/>
|
||||
</module>
|
||||
<!-- Suppression for block code until we find a way to detect them properly
|
||||
until https://github.com/checkstyle/checkstyle/issues/15769 -->
|
||||
<module name="SuppressionXpathSingleFilter">
|
||||
<property name="checks" value="Indentation"/>
|
||||
<property name="query" value="//SLIST[not(parent::CASE_GROUP)]/SLIST
|
||||
| //SLIST[not(parent::CASE_GROUP)]/SLIST/RCURLY"/>
|
||||
</module>
|
||||
<module name="AbbreviationAsWordInName">
|
||||
<property name="ignoreFinal" value="false"/>
|
||||
<property name="ignoreFinal" value="true"/>
|
||||
<property name="allowedAbbreviations" value="API" />
|
||||
<property name="allowedAbbreviationLength" value="1"/>
|
||||
<property name="tokens"
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
|
||||
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF"/>
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, ANNOTATION_DEF, ANNOTATION_FIELD_DEF,
|
||||
PARAMETER_DEF, VARIABLE_DEF, METHOD_DEF, PATTERN_VARIABLE_DEF, RECORD_DEF,
|
||||
RECORD_COMPONENT_DEF"/>
|
||||
</module>
|
||||
<module name="NoWhitespaceBeforeCaseDefaultColon"/>
|
||||
<module name="OverloadMethodsDeclarationOrder"/>
|
||||
<!--<module name="ConstructorsDeclarationGrouping"/>-->
|
||||
<module name="VariableDeclarationUsageDistance"/>
|
||||
<module name="CustomImportOrder">
|
||||
<property name="sortImportsInGroupAlphabetically" value="true"/>
|
||||
<property name="separateLineBetweenGroups" value="true"/>
|
||||
<property name="customImportOrderRules" value="STATIC###THIRD_PARTY_PACKAGE"/>
|
||||
<property name="tokens" value="IMPORT, STATIC_IMPORT, PACKAGE_DEF"/>
|
||||
</module>
|
||||
<module name="MethodParamPad">
|
||||
<property name="tokens"
|
||||
value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF,
|
||||
value="CTOR_DEF, LITERAL_NEW, METHOD_CALL, METHOD_DEF,
|
||||
SUPER_CTOR_CALL, ENUM_CONSTANT_DEF"/>
|
||||
</module>
|
||||
<module name="NoWhitespaceBefore">
|
||||
<property name="tokens"
|
||||
value="COMMA, SEMI, POST_INC, POST_DEC, DOT, ELLIPSIS, METHOD_REF"/>
|
||||
value="COMMA, SEMI, POST_INC, POST_DEC, DOT,
|
||||
LABELED_STAT, METHOD_REF"/>
|
||||
<property name="allowLineBreaks" value="true"/>
|
||||
</module>
|
||||
<module name="ParenPad">
|
||||
<property name="tokens"
|
||||
value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_CALL, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,
|
||||
EXPR, LITERAL_CATCH, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,
|
||||
LITERAL_SWITCH, LITERAL_SYNCHRONIZED, LITERAL_WHILE, METHOD_CALL,
|
||||
METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL, LAMBDA"/>
|
||||
value="ANNOTATION, ANNOTATION_FIELD_DEF, CTOR_DEF, DOT, ENUM_CONSTANT_DEF,
|
||||
EXPR, LITERAL_DO, LITERAL_FOR, LITERAL_IF, LITERAL_NEW,
|
||||
LITERAL_WHILE, METHOD_CALL,
|
||||
METHOD_DEF, QUESTION, RESOURCE_SPECIFICATION, SUPER_CTOR_CALL"/>
|
||||
</module>
|
||||
<module name="OperatorWrap">
|
||||
<property name="option" value="NL"/>
|
||||
<property name="tokens"
|
||||
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
|
||||
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF "/>
|
||||
value="BAND, BOR, BSR, BXOR, DIV, EQUAL, GE, GT, LAND, LE, LITERAL_INSTANCEOF, LOR,
|
||||
LT, MINUS, MOD, NOT_EQUAL, PLUS, QUESTION, SL, SR, STAR, METHOD_REF,
|
||||
TYPE_EXTENSION_AND "/>
|
||||
</module>
|
||||
<module name="AnnotationLocation">
|
||||
<property name="id" value="AnnotationLocationMostCases"/>
|
||||
<property name="tokens"
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF"/>
|
||||
<property name="allowSamelineMultipleAnnotations" value="true"/>
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF,
|
||||
RECORD_DEF, COMPACT_CTOR_DEF"/>
|
||||
</module>
|
||||
<module name="AnnotationLocation">
|
||||
<property name="id" value="AnnotationLocationVariables"/>
|
||||
|
@ -271,47 +369,83 @@
|
|||
<module name="JavadocTagContinuationIndentation"/>
|
||||
<module name="SummaryJavadoc">
|
||||
<property name="forbiddenSummaryFragments"
|
||||
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
|
||||
value="^@return the *|^This method returns |^A [{]@code [a-zA-Z0-9]+[}]( is a )"/>
|
||||
</module>
|
||||
<module name="JavadocParagraph"/>
|
||||
<module name="JavadocParagraph">
|
||||
</module>
|
||||
<module name="RequireEmptyLineBeforeBlockTagGroup"/>
|
||||
<module name="AtclauseOrder">
|
||||
<property name="tagOrder" value="@param, @return, @throws, @deprecated"/>
|
||||
<property name="target"
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF, METHOD_DEF, CTOR_DEF, VARIABLE_DEF"/>
|
||||
</module>
|
||||
<module name="JavadocMethod">
|
||||
<property name="scope" value="public"/>
|
||||
<property name="accessModifiers" value="public"/>
|
||||
<property name="allowMissingParamTags" value="true"/>
|
||||
<property name="allowMissingReturnTag" value="true"/>
|
||||
<property name="allowedAnnotations" value="Override, Test"/>
|
||||
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF"/>
|
||||
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF, COMPACT_CTOR_DEF"/>
|
||||
</module>
|
||||
<module name="MissingJavadocMethod">
|
||||
<property name="scope" value="public"/>
|
||||
<property name="minLineCount" value="2"/>
|
||||
<property name="allowMissingPropertyJavadoc" value="true"/>
|
||||
<property name="allowedAnnotations" value="Override, Test"/>
|
||||
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF"/>
|
||||
<property name="tokens" value="METHOD_DEF, CTOR_DEF, ANNOTATION_FIELD_DEF,
|
||||
COMPACT_CTOR_DEF"/>
|
||||
</module>
|
||||
<module name="SuppressionXpathSingleFilter">
|
||||
<property name="checks" value="MissingJavadocMethod"/>
|
||||
<property name="query" value="//*[self::METHOD_DEF or self::CTOR_DEF
|
||||
or self::ANNOTATION_FIELD_DEF or self::COMPACT_CTOR_DEF]
|
||||
[ancestor::*[self::INTERFACE_DEF or self::CLASS_DEF
|
||||
or self::RECORD_DEF or self::ENUM_DEF]
|
||||
[not(./MODIFIERS/LITERAL_PUBLIC)]]"/>
|
||||
</module>
|
||||
<module name="MissingJavadocType">
|
||||
<property name="scope" value="protected"/>
|
||||
<property name="tokens"
|
||||
value="CLASS_DEF, INTERFACE_DEF, ENUM_DEF,
|
||||
RECORD_DEF, ANNOTATION_DEF"/>
|
||||
<property name="excludeScope" value="nothing"/>
|
||||
</module>
|
||||
<module name="MethodName">
|
||||
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9_]*$"/>
|
||||
<property name="format" value="^[a-z][a-z0-9][a-zA-Z0-9]*$"/>
|
||||
<message key="name.invalidPattern"
|
||||
value="Method name ''{0}'' must match pattern ''{1}''."/>
|
||||
value="Method name ''{0}'' must match pattern ''{1}''."/>
|
||||
</module>
|
||||
<module name="SingleLineJavadoc">
|
||||
<property name="ignoreInlineTags" value="false"/>
|
||||
<module name="SuppressionXpathSingleFilter">
|
||||
<property name="checks" value="MethodName"/>
|
||||
<property name="query" value="//METHOD_DEF[
|
||||
./MODIFIERS/ANNOTATION//IDENT[contains(@text, 'Test')]
|
||||
]/IDENT"/>
|
||||
<property name="message" value="'[a-z][a-z0-9][a-zA-Z0-9]*(?:_[a-z][a-z0-9][a-zA-Z0-9]*)*'"/>
|
||||
</module>
|
||||
<module name="SingleLineJavadoc"/>
|
||||
<module name="EmptyCatchBlock">
|
||||
<property name="exceptionVariableName" value="expected"/>
|
||||
</module>
|
||||
<module name="CommentsIndentation">
|
||||
<property name="tokens" value="SINGLE_LINE_COMMENT, BLOCK_COMMENT_BEGIN"/>
|
||||
</module>
|
||||
<!-- https://checkstyle.org/config_filters.html#SuppressionXpathFilter -->
|
||||
<!-- https://checkstyle.org/filters/suppressionxpathfilter.html -->
|
||||
<module name="SuppressionXpathFilter">
|
||||
<property name="file" value="${org.checkstyle.google.suppressionxpathfilter.config}"
|
||||
default="checkstyle-xpath-suppressions.xml" />
|
||||
<property name="optional" value="true"/>
|
||||
</module>
|
||||
<module name="SuppressWarningsHolder" />
|
||||
<module name="SuppressionCommentFilter">
|
||||
<property name="offCommentFormat" value="CHECKSTYLE.OFF\: ([\w\|]+)" />
|
||||
<property name="onCommentFormat" value="CHECKSTYLE.ON\: ([\w\|]+)" />
|
||||
<property name="checkFormat" value="$1" />
|
||||
</module>
|
||||
<module name="SuppressWithNearbyCommentFilter">
|
||||
<property name="commentFormat" value="CHECKSTYLE.SUPPRESS\: ([\w\|]+)"/>
|
||||
<!-- $1 refers to the first match group in the regex defined in commentFormat -->
|
||||
<property name="checkFormat" value="$1"/>
|
||||
<!-- The check is suppressed in the next line of code after the comment -->
|
||||
<property name="influenceFormat" value="1"/>
|
||||
</module>
|
||||
</module>
|
||||
</module>
|
||||
|
||||
|
|
|
@ -0,0 +1,11 @@
|
|||
# Releases
|
||||
|
||||
This repo uses _Release Please_ to release packages. Release Please sets up a running PR that tracks all changes in the library, and maintains the versions according to [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/), generated when [PRs are merged](https://github.com/amannn/action-semantic-pull-request), based on the PR title. The semantics of the PR title are enforced by the `lint-pr.yml` workflow. When Release Please's running PR is merged, a new release is created, and the associated artifacts are published.
|
||||
|
||||
## Customization of changelog and release notes.
|
||||
|
||||
If you'd like to add custom content to a release, you can do this by editing the content in a Release Please PR's description. This content will be added to the notes for that release. If you'd like to add content to the changelog, simply push updates to the changelog in the Release Please PR.
|
||||
|
||||
## Configuration
|
||||
|
||||
The `release-please-config.json` defines the release please configuration. See schema [here](https://github.com/googleapis/release-please/blob/main/schemas/config.json) to understand all the options. We use the "simple" release strategy and annotate the POM with an element to help release please find the correct XML entity to update (the version element). Release Please stores it's understanding of the current version in the `version.txt` file.
|
|
@ -0,0 +1,259 @@
|
|||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
|
@ -0,0 +1,149 @@
|
|||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.2
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace '^.*'+$MVNW_REPO_PATTERN,'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
$MAVEN_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain"
|
||||
}
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
|
@ -0,0 +1,73 @@
|
|||
{
|
||||
"bootstrap-sha": "d7b591c9f910afad303d6d814f65c7f9dab33b89",
|
||||
"signoff": "OpenFeature Bot <109696520+openfeaturebot@users.noreply.github.com>",
|
||||
"packages": {
|
||||
".": {
|
||||
"package-name": "dev.openfeature.sdk",
|
||||
"monorepo-tags": false,
|
||||
"release-type": "simple",
|
||||
"include-component-in-tag": false,
|
||||
"bump-minor-pre-major": true,
|
||||
"bump-patch-for-minor-pre-major": true,
|
||||
"versioning": "default",
|
||||
"extra-files": [
|
||||
"pom.xml",
|
||||
"README.md"
|
||||
],
|
||||
"changelog-sections": [
|
||||
{
|
||||
"type": "fix",
|
||||
"section": "🐛 Bug Fixes"
|
||||
},
|
||||
{
|
||||
"type": "feat",
|
||||
"section": "✨ New Features"
|
||||
},
|
||||
{
|
||||
"type": "chore",
|
||||
"section": "🧹 Chore"
|
||||
},
|
||||
{
|
||||
"type": "docs",
|
||||
"section": "📚 Documentation"
|
||||
},
|
||||
{
|
||||
"type": "perf",
|
||||
"section": "🚀 Performance"
|
||||
},
|
||||
{
|
||||
"type": "build",
|
||||
"hidden": true,
|
||||
"section": "🛠️ Build"
|
||||
},
|
||||
{
|
||||
"type": "deps",
|
||||
"section": "📦 Dependencies"
|
||||
},
|
||||
{
|
||||
"type": "ci",
|
||||
"hidden": true,
|
||||
"section": "🚦 CI"
|
||||
},
|
||||
{
|
||||
"type": "refactor",
|
||||
"section": "🔄 Refactoring"
|
||||
},
|
||||
{
|
||||
"type": "revert",
|
||||
"section": "🔙 Reverts"
|
||||
},
|
||||
{
|
||||
"type": "style",
|
||||
"hidden": true,
|
||||
"section": "🎨 Styling"
|
||||
},
|
||||
{
|
||||
"type": "test",
|
||||
"hidden": true,
|
||||
"section": "🧪 Tests"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
|
@ -5,5 +5,10 @@
|
|||
<username>${env.OSSRH_USERNAME}</username>
|
||||
<password>${env.OSSRH_PASSWORD}</password>
|
||||
</server>
|
||||
<server>
|
||||
<id>central</id>
|
||||
<username>${env.CENTRAL_USERNAME}</username>
|
||||
<password>${env.CENTRAL_PASSWORD}</password>
|
||||
</server>
|
||||
</servers>
|
||||
</settings>
|
||||
|
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
||||
"extends": [
|
||||
"config:base"
|
||||
],
|
||||
"packageRules": [
|
||||
{
|
||||
"matchUpdateTypes": ["minor", "patch"],
|
||||
"matchCurrentVersion": "!/^0/",
|
||||
"automerge": true
|
||||
},
|
||||
{
|
||||
"matchManagers": ["github-actions"],
|
||||
"automerge": true
|
||||
}
|
||||
],
|
||||
"regexManagers": [
|
||||
{
|
||||
"fileMatch": ["^README.md$", "^.github/workflows/pullrequest.yml$"],
|
||||
"matchStrings": ["ghcr\\.io\\/open-feature\\/flagd-testbed:(?<currentValue>.*?)\\n"],
|
||||
"depNameTemplate": "open-feature/test-harness",
|
||||
"datasourceTemplate": "github-releases"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
Subproject commit d4a9a910946eded57cf82d6fd4921785a5e64c2b
|
111
spec_finder.py
111
spec_finder.py
|
@ -1,111 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
import urllib.request
|
||||
import json
|
||||
import re
|
||||
import difflib
|
||||
import os
|
||||
import sys
|
||||
|
||||
def _demarkdown(t):
|
||||
return t.replace('**', '').replace('`', '').replace('"', '')
|
||||
|
||||
def get_spec(force_refresh=False):
|
||||
spec_path = './specification.json'
|
||||
data = ""
|
||||
if os.path.exists(spec_path):
|
||||
with open(spec_path) as f:
|
||||
data = ''.join(f.readlines())
|
||||
else:
|
||||
# TODO: Status code check
|
||||
spec_response = urllib.request.urlopen('https://raw.githubusercontent.com/open-feature/spec/main/specification.json')
|
||||
raw = []
|
||||
for i in spec_response.readlines():
|
||||
raw.append(i.decode('utf-8'))
|
||||
data = ''.join(raw)
|
||||
with open(spec_path, 'w') as f:
|
||||
f.write(data)
|
||||
return json.loads(data)
|
||||
|
||||
|
||||
def main(refresh_spec=False, diff_output=False, limit_numbers=None):
|
||||
actual_spec = get_spec(refresh_spec)
|
||||
|
||||
spec_map = {}
|
||||
for entry in actual_spec['rules']:
|
||||
number = re.search('[\d.]+', entry['id']).group()
|
||||
if 'requirement' in entry['machine_id']:
|
||||
spec_map[number] = _demarkdown(entry['content'])
|
||||
|
||||
if len(entry['children']) > 0:
|
||||
for ch in entry['children']:
|
||||
number = re.search('[\d.]+', ch['id']).group()
|
||||
if 'requirement' in ch['machine_id']:
|
||||
spec_map[number] = _demarkdown(ch['content'])
|
||||
|
||||
java_specs = {}
|
||||
missing = set(spec_map.keys())
|
||||
|
||||
|
||||
import os
|
||||
for root, dirs, files in os.walk(".", topdown=False):
|
||||
for name in files:
|
||||
F = os.path.join(root, name)
|
||||
if '.java' not in name:
|
||||
continue
|
||||
with open(F) as f:
|
||||
data = ''.join(f.readlines())
|
||||
|
||||
for match in re.findall('@Specification\((?P<innards>.*?)"\)', data.replace('\n', ''), re.MULTILINE | re.DOTALL):
|
||||
number = re.findall('number\s*=\s*"(.*?)"', match)[0]
|
||||
|
||||
if number in missing:
|
||||
missing.remove(number)
|
||||
text_with_concat_chars = re.findall('text\s*=\s*(.*)', match)
|
||||
try:
|
||||
# We have to match for ") to capture text with parens inside, so we add the trailing " back in.
|
||||
text = _demarkdown(eval(''.join(text_with_concat_chars) + '"'))
|
||||
entry = java_specs[number] = {
|
||||
'number': number,
|
||||
'text': text,
|
||||
}
|
||||
except:
|
||||
print(f"Skipping {match} b/c we couldn't parse it")
|
||||
|
||||
bad_num = len(missing)
|
||||
for number, entry in java_specs.items():
|
||||
if limit_numbers is not None and len(limit_numbers) > 0 and number not in limit_numbers:
|
||||
continue
|
||||
if number in spec_map:
|
||||
txt = entry['text']
|
||||
if txt == spec_map[number]:
|
||||
# print(f'{number} is good')
|
||||
continue
|
||||
else:
|
||||
print(f"{number} is bad")
|
||||
bad_num += 1
|
||||
if diff_output:
|
||||
print(number + '\n' + '\n'.join([li for li in difflib.ndiff([txt], [spec_map[number]]) if not li.startswith(' ')]))
|
||||
continue
|
||||
|
||||
print(f"{number} is defined in our tests, but couldn't find it in the spec")
|
||||
print("")
|
||||
|
||||
if len(missing) > 0:
|
||||
print('In the spec, but not in our tests: ')
|
||||
for m in missing:
|
||||
print(f" {m}: {spec_map[m]}")
|
||||
|
||||
sys.exit(bad_num)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
import argparse
|
||||
|
||||
parser = argparse.ArgumentParser(description='Parse the spec to make sure our tests cover it')
|
||||
parser.add_argument('--refresh-spec', action='store_true', help='Re-downloads the spec')
|
||||
parser.add_argument('--diff-output', action='store_true', help='print the text differences')
|
||||
parser.add_argument('specific_numbers', metavar='num', type=str, nargs='*',
|
||||
help='limit this to specific numbers')
|
||||
|
||||
args = parser.parse_args()
|
||||
main(refresh_spec=args.refresh_spec, diff_output=args.diff_output, limit_numbers=args.specific_numbers)
|
|
@ -6,18 +6,50 @@
|
|||
|
||||
<!-- I'm reasonably confident that the singleton pattern isn't exposing internal representation -->
|
||||
<And>
|
||||
<Class name="dev.openfeature.javasdk.OpenFeatureAPI"/>
|
||||
<Class name="dev.openfeature.sdk.OpenFeatureAPI"/>
|
||||
<Bug pattern="MS_EXPOSE_REP"/>
|
||||
</And>
|
||||
<!-- similarly, client using the singleton doesn't seem bad -->
|
||||
<!-- evaluation context and hooks are mutable if mutable impl is used -->
|
||||
<And>
|
||||
<Class name="dev.openfeature.javasdk.OpenFeatureClient"/>
|
||||
<Class name="dev.openfeature.sdk.OpenFeatureClient"/>
|
||||
<Bug pattern="EI_EXPOSE_REP"/>
|
||||
</And>
|
||||
<And>
|
||||
<Class name="dev.openfeature.sdk.OpenFeatureClient"/>
|
||||
<Bug pattern="EI_EXPOSE_REP2"/>
|
||||
</And>
|
||||
<And>
|
||||
<Class name="dev.openfeature.sdk.OpenFeatureAPI"/>
|
||||
<Bug pattern="EI_EXPOSE_REP"/>
|
||||
</And>
|
||||
<And>
|
||||
<Class name="dev.openfeature.sdk.OpenFeatureAPI"/>
|
||||
<Bug pattern="EI_EXPOSE_REP2"/>
|
||||
</And>
|
||||
<And>
|
||||
Added in spotbugs 4.8.0 - EventProvider shares a name with something from the standard lib (confusing), but change would be breaking
|
||||
<Class name="dev.openfeature.sdk.EventProvider"/>
|
||||
<Bug pattern="PI_DO_NOT_REUSE_PUBLIC_IDENTIFIERS_CLASS_NAMES"/>
|
||||
</And>
|
||||
<And>
|
||||
Added in spotbugs 4.8.0 - Metadata shares a name with something from the standard lib (confusing), but change would be breaking
|
||||
<Class name="dev.openfeature.sdk.Metadata"/>
|
||||
<Bug pattern="PI_DO_NOT_REUSE_PUBLIC_IDENTIFIERS_CLASS_NAMES"/>
|
||||
</And>
|
||||
<And>
|
||||
Added in spotbugs 4.8.0 - Reason shares a name with something from the standard lib (confusing), but change would be breaking
|
||||
<Class name="dev.openfeature.sdk.Reason"/>
|
||||
<Bug pattern="PI_DO_NOT_REUSE_PUBLIC_IDENTIFIERS_CLASS_NAMES"/>
|
||||
</And>
|
||||
<And>
|
||||
Added in spotbugs 4.8.0 - FlagValueType.STRING shares a name with something from the standard lib (confusing), but change would be breaking
|
||||
<Class name="dev.openfeature.sdk.FlagValueType"/>
|
||||
<Bug pattern="PI_DO_NOT_REUSE_PUBLIC_IDENTIFIERS_FIELD_NAMES"/>
|
||||
</And>
|
||||
|
||||
<!-- Test class that should be excluded -->
|
||||
<Match>
|
||||
<Class name="dev.openfeature.javasdk.DoSomethingProvider"/>
|
||||
<Class name="dev.openfeature.sdk.DoSomethingProvider"/>
|
||||
</Match>
|
||||
<!-- All bugs in test classes, except for JUnit-specific bugs -->
|
||||
<Match>
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
public interface BooleanHook extends Hook<Boolean> {
|
||||
|
||||
@Override
|
||||
default boolean supportsFlagValueType(FlagValueType flagValueType) {
|
||||
return FlagValueType.BOOLEAN == flagValueType;
|
||||
}
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
public enum ErrorCode {
|
||||
PROVIDER_NOT_READY, FLAG_NOT_FOUND, PARSE_ERROR, TYPE_MISMATCH, GENERAL
|
||||
}
|
|
@ -1,146 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
import dev.openfeature.javasdk.internal.Pair;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.ZonedDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
@ToString @EqualsAndHashCode
|
||||
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
|
||||
public class EvaluationContext {
|
||||
@Setter @Getter private String targetingKey;
|
||||
private final Map<String, Pair<FlagValueType, Object>> attributes;
|
||||
|
||||
public EvaluationContext() {
|
||||
this.targetingKey = "";
|
||||
this.attributes = new HashMap<>();
|
||||
}
|
||||
|
||||
// TODO Not sure if I should have sneakythrows or checked exceptions here..
|
||||
@SneakyThrows
|
||||
public <T> EvaluationContext addStructureAttribute(String key, T value) {
|
||||
attributes.put(key, new Pair<>(FlagValueType.OBJECT, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
@SneakyThrows
|
||||
public <T> T getStructureAttribute(String key) {
|
||||
return getAttributeByType(key, FlagValueType.OBJECT);
|
||||
}
|
||||
|
||||
public Map<String, String> getStructureAttributes() {
|
||||
return getAttributesByType(FlagValueType.OBJECT);
|
||||
}
|
||||
|
||||
public EvaluationContext addStringAttribute(String key, String value) {
|
||||
attributes.put(key, new Pair<>(FlagValueType.STRING, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public String getStringAttribute(String key) {
|
||||
return getAttributeByType(key, FlagValueType.STRING);
|
||||
}
|
||||
|
||||
private <T> Map<String, T> getAttributesByType(FlagValueType type) {
|
||||
HashMap<String, T> hm = new HashMap<>();
|
||||
for (Map.Entry<String, Pair<FlagValueType, Object>> entry : attributes.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
if (entry.getValue().getFirst() == type) {
|
||||
hm.put(key, (T) entry.getValue().getSecond());
|
||||
}
|
||||
}
|
||||
return hm;
|
||||
}
|
||||
|
||||
private <T> T getAttributeByType(String key, FlagValueType type) {
|
||||
Pair<FlagValueType, Object> val = attributes.get(key);
|
||||
if (val == null) {
|
||||
return null;
|
||||
}
|
||||
if (val.getFirst() == type) {
|
||||
return (T) val.getSecond();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public Map<String, String> getStringAttributes() {
|
||||
return getAttributesByType(FlagValueType.STRING);
|
||||
}
|
||||
|
||||
public EvaluationContext addIntegerAttribute(String key, Integer value) {
|
||||
attributes.put(key, new Pair<>(FlagValueType.INTEGER, value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Integer getIntegerAttribute(String key) {
|
||||
return getAttributeByType(key, FlagValueType.INTEGER);
|
||||
}
|
||||
|
||||
public Map<String, Integer> getIntegerAttributes() {
|
||||
return getAttributesByType(FlagValueType.INTEGER);
|
||||
}
|
||||
|
||||
public Boolean getBooleanAttribute(String key) {
|
||||
return getAttributeByType(key, FlagValueType.BOOLEAN);
|
||||
}
|
||||
|
||||
public EvaluationContext addBooleanAttribute(String key, Boolean b) {
|
||||
attributes.put(key, new Pair<>(FlagValueType.BOOLEAN, b));
|
||||
return this;
|
||||
}
|
||||
|
||||
public Map<String, Boolean> getBooleanAttributes() {
|
||||
return getAttributesByType(FlagValueType.BOOLEAN);
|
||||
}
|
||||
|
||||
public EvaluationContext addDatetimeAttribute(String key, ZonedDateTime value) {
|
||||
attributes.put(key, new Pair<>(FlagValueType.STRING, value.format(DateTimeFormatter.ISO_ZONED_DATE_TIME)));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch date-time relevant key.
|
||||
* @param key feature key
|
||||
* @return date time object.
|
||||
* @throws java.time.format.DateTimeParseException if it's not a datetime
|
||||
*/
|
||||
public ZonedDateTime getDatetimeAttribute(String key) {
|
||||
String attr = getAttributeByType(key, FlagValueType.STRING);
|
||||
if (attr == null) {
|
||||
return null;
|
||||
}
|
||||
return ZonedDateTime.parse(attr, DateTimeFormatter.ISO_ZONED_DATE_TIME);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges two EvaluationContext objects with the second overriding the first in case of conflict.
|
||||
*
|
||||
* @param ctx1 base context
|
||||
* @param ctx2 overriding context
|
||||
* @return resulting merged context
|
||||
*/
|
||||
public static EvaluationContext merge(EvaluationContext ctx1, EvaluationContext ctx2) {
|
||||
EvaluationContext ec = new EvaluationContext();
|
||||
if (ctx1 == null) {
|
||||
return ctx2;
|
||||
} else if (ctx2 == null) {
|
||||
return ctx1;
|
||||
}
|
||||
|
||||
ec.attributes.putAll(ctx1.attributes);
|
||||
ec.attributes.putAll(ctx2.attributes);
|
||||
|
||||
if (ctx1.getTargetingKey() != null) {
|
||||
ec.setTargetingKey(ctx1.getTargetingKey());
|
||||
}
|
||||
|
||||
if (ctx2.getTargetingKey() != null) {
|
||||
ec.setTargetingKey(ctx2.getTargetingKey());
|
||||
}
|
||||
|
||||
return ec;
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The interface implemented by upstream flag providers to resolve flags for their service.
|
||||
*/
|
||||
public interface FeatureProvider {
|
||||
Metadata getMetadata();
|
||||
|
||||
default List<Hook> getProviderHooks() {
|
||||
return Lists.newArrayList();
|
||||
}
|
||||
|
||||
ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
|
||||
ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
|
||||
ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
|
||||
ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
|
||||
<T> ProviderEvaluation<T> getObjectEvaluation(String key, T defaultValue, EvaluationContext invocationContext,
|
||||
FlagEvaluationOptions options);
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
/**
|
||||
* Contains information about how the evaluation happened, including any resolved values.
|
||||
* @param <T> the type of the flag being evaluated.
|
||||
*/
|
||||
@Data @Builder
|
||||
public class FlagEvaluationDetails<T> implements BaseEvaluation<T> {
|
||||
private String flagKey;
|
||||
private T value;
|
||||
@Nullable private String variant;
|
||||
private Reason reason;
|
||||
@Nullable private String errorCode;
|
||||
|
||||
/**
|
||||
* Generate detail payload from the provider response.
|
||||
* @param providerEval provider response
|
||||
* @param flagKey key for the flag being evaluated
|
||||
* @param <T> type of flag being returned
|
||||
* @return detail payload
|
||||
*/
|
||||
public static <T> FlagEvaluationDetails<T> from(ProviderEvaluation<T> providerEval, String flagKey) {
|
||||
return FlagEvaluationDetails.<T>builder()
|
||||
.flagKey(flagKey)
|
||||
.value(providerEval.getValue())
|
||||
.variant(providerEval.getVariant())
|
||||
.reason(providerEval.getReason())
|
||||
.errorCode(providerEval.getErrorCode())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
@Value
|
||||
@Builder
|
||||
public class FlagEvaluationOptions {
|
||||
@Singular
|
||||
List<Hook> hooks;
|
||||
@Builder.Default
|
||||
Map<String, Object> hookHints = new HashMap<>();
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
public enum FlagValueType {
|
||||
STRING, INTEGER, DOUBLE, OBJECT, BOOLEAN;
|
||||
}
|
|
@ -1,90 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
import com.google.common.collect.Lists;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
class HookSupport {
|
||||
|
||||
public void errorHooks(FlagValueType flagValueType, HookContext hookCtx, Exception e, List<Hook> hooks,
|
||||
Map<String, Object> hints) {
|
||||
executeHooks(flagValueType, hooks, "error", hook -> hook.error(hookCtx, e, hints));
|
||||
}
|
||||
|
||||
public void afterAllHooks(FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks,
|
||||
Map<String, Object> hints) {
|
||||
executeHooks(flagValueType, hooks, "finally", hook -> hook.finallyAfter(hookCtx, hints));
|
||||
}
|
||||
|
||||
public void afterHooks(FlagValueType flagValueType, HookContext hookContext, FlagEvaluationDetails details,
|
||||
List<Hook> hooks, Map<String, Object> hints) {
|
||||
executeHooksUnchecked(flagValueType, hooks, hook -> hook.after(hookContext, details, hints));
|
||||
}
|
||||
|
||||
private <T> void executeHooks(
|
||||
FlagValueType flagValueType, List<Hook> hooks,
|
||||
String hookMethod,
|
||||
Consumer<Hook<T>> hookCode
|
||||
) {
|
||||
hooks
|
||||
.stream()
|
||||
.filter(hook -> hook.supportsFlagValueType(flagValueType))
|
||||
.forEach(hook -> executeChecked(hook, hookCode, hookMethod));
|
||||
}
|
||||
|
||||
private <T> void executeHooksUnchecked(
|
||||
FlagValueType flagValueType, List<Hook> hooks,
|
||||
Consumer<Hook<T>> hookCode
|
||||
) {
|
||||
hooks
|
||||
.stream()
|
||||
.filter(hook -> hook.supportsFlagValueType(flagValueType))
|
||||
.forEach(hookCode::accept);
|
||||
}
|
||||
|
||||
private <T> void executeChecked(Hook<T> hook, Consumer<Hook<T>> hookCode, String hookMethod) {
|
||||
try {
|
||||
hookCode.accept(hook);
|
||||
} catch (Exception exception) {
|
||||
log.error("Exception when running {} hooks {}", hookMethod, hook.getClass(), exception);
|
||||
}
|
||||
}
|
||||
|
||||
public EvaluationContext beforeHooks(FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks,
|
||||
Map<String, Object> hints) {
|
||||
Stream<EvaluationContext> result = callBeforeHooks(flagValueType, hookCtx, hooks, hints);
|
||||
return EvaluationContext.merge(hookCtx.getCtx(), collectContexts(result));
|
||||
}
|
||||
|
||||
private Stream<EvaluationContext> callBeforeHooks(FlagValueType flagValueType, HookContext hookCtx,
|
||||
List<Hook> hooks, Map<String, Object> hints) {
|
||||
// These traverse backwards from normal.
|
||||
return Lists
|
||||
.reverse(hooks)
|
||||
.stream()
|
||||
.filter(hook -> hook.supportsFlagValueType(flagValueType))
|
||||
.map(hook -> hook.before(hookCtx, hints))
|
||||
.filter(Objects::nonNull)
|
||||
.filter(Optional::isPresent)
|
||||
.map(Optional::get)
|
||||
.map(EvaluationContext.class::cast);
|
||||
}
|
||||
|
||||
//for whatever reason, an error `error: incompatible types: invalid method reference` is thrown on compilation
|
||||
// with javac
|
||||
//when the reduce call is appended directly as stream call chain above. moving it to its own method works however...
|
||||
private EvaluationContext collectContexts(Stream<EvaluationContext> result) {
|
||||
return result
|
||||
.reduce(new EvaluationContext(), EvaluationContext::merge, EvaluationContext::merge);
|
||||
}
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
public interface IntegerHook extends Hook<Integer> {
|
||||
|
||||
@Override
|
||||
default boolean supportsFlagValueType(FlagValueType flagValueType) {
|
||||
return FlagValueType.INTEGER == flagValueType;
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* A global singleton which holds base configuration for the OpenFeature library.
|
||||
* Configuration here will be shared across all {@link Client}s.
|
||||
*/
|
||||
public class OpenFeatureAPI {
|
||||
private static OpenFeatureAPI api;
|
||||
@Getter
|
||||
@Setter
|
||||
private FeatureProvider provider;
|
||||
@Getter
|
||||
@Setter
|
||||
private EvaluationContext ctx;
|
||||
@Getter
|
||||
private List<Hook> apiHooks;
|
||||
|
||||
public OpenFeatureAPI() {
|
||||
this.apiHooks = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provisions the {@link OpenFeatureAPI} singleton (if needed) and returns it.
|
||||
* @return The singleton instance.
|
||||
*/
|
||||
public static OpenFeatureAPI getInstance() {
|
||||
synchronized (OpenFeatureAPI.class) {
|
||||
if (api == null) {
|
||||
api = new OpenFeatureAPI();
|
||||
}
|
||||
}
|
||||
return api;
|
||||
}
|
||||
|
||||
public Metadata getProviderMetadata() {
|
||||
return provider.getMetadata();
|
||||
}
|
||||
|
||||
public Client getClient() {
|
||||
return getClient(null, null);
|
||||
}
|
||||
|
||||
public Client getClient(@Nullable String name) {
|
||||
return getClient(name, null);
|
||||
}
|
||||
|
||||
public Client getClient(@Nullable String name, @Nullable String version) {
|
||||
return new OpenFeatureClient(this, name, version);
|
||||
}
|
||||
|
||||
public void addHooks(Hook... hooks) {
|
||||
this.apiHooks.addAll(Arrays.asList(hooks));
|
||||
}
|
||||
|
||||
public void clearHooks() {
|
||||
this.apiHooks.clear();
|
||||
}
|
||||
}
|
|
@ -1,286 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
import dev.openfeature.javasdk.exceptions.GeneralError;
|
||||
import dev.openfeature.javasdk.internal.ObjectUtils;
|
||||
import lombok.Getter;
|
||||
import lombok.Setter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Slf4j
|
||||
@SuppressWarnings({"PMD.DataflowAnomalyAnalysis", "PMD.BeanMembersShouldSerialize", "unchecked", "rawtypes"})
|
||||
public class OpenFeatureClient implements Client {
|
||||
|
||||
private final OpenFeatureAPI openfeatureApi;
|
||||
@Getter
|
||||
private final String name;
|
||||
@Getter
|
||||
private final String version;
|
||||
@Getter
|
||||
private final List<Hook> clientHooks;
|
||||
private final HookSupport hookSupport;
|
||||
|
||||
@Getter
|
||||
@Setter
|
||||
private EvaluationContext evaluationContext;
|
||||
|
||||
/**
|
||||
* Client for evaluating the flag. There may be multiples of these floating around.
|
||||
* @param openFeatureAPI Backing global singleton
|
||||
* @param name Name of the client (used by observability tools).
|
||||
* @param version Version of the client (used by observability tools).
|
||||
*/
|
||||
public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String name, String version) {
|
||||
this.openfeatureApi = openFeatureAPI;
|
||||
this.name = name;
|
||||
this.version = version;
|
||||
this.clientHooks = new ArrayList<>();
|
||||
this.hookSupport = new HookSupport();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addHooks(Hook... hooks) {
|
||||
this.clientHooks.addAll(Arrays.asList(hooks));
|
||||
}
|
||||
|
||||
private <T> FlagEvaluationDetails<T> evaluateFlag(FlagValueType type, String key, T defaultValue,
|
||||
EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
FlagEvaluationOptions flagOptions = ObjectUtils.defaultIfNull(options,
|
||||
() -> FlagEvaluationOptions.builder().build());
|
||||
FeatureProvider provider = openfeatureApi.getProvider();
|
||||
Map<String, Object> hints = Collections.unmodifiableMap(flagOptions.getHookHints());
|
||||
if (ctx == null) {
|
||||
ctx = new EvaluationContext();
|
||||
}
|
||||
|
||||
HookContext<T> hookCtx = HookContext.from(key, type, this.getMetadata(),
|
||||
openfeatureApi.getProvider().getMetadata(), ctx, defaultValue);
|
||||
|
||||
List<Hook> mergedHooks = ObjectUtils.merge(provider.getProviderHooks(), flagOptions.getHooks(), clientHooks,
|
||||
openfeatureApi.getApiHooks());
|
||||
|
||||
FlagEvaluationDetails<T> details = null;
|
||||
try {
|
||||
EvaluationContext ctxFromHook = hookSupport.beforeHooks(type, hookCtx, mergedHooks, hints);
|
||||
|
||||
EvaluationContext invocationCtx = EvaluationContext.merge(ctx, ctxFromHook);
|
||||
|
||||
// merge of: API.context, client.context, invocation.context
|
||||
EvaluationContext mergedCtx = EvaluationContext.merge(
|
||||
EvaluationContext.merge(
|
||||
openfeatureApi.getCtx(),
|
||||
this.getEvaluationContext()
|
||||
),
|
||||
invocationCtx
|
||||
);
|
||||
|
||||
ProviderEvaluation<T> providerEval = (ProviderEvaluation<T>) createProviderEvaluation(type, key,
|
||||
defaultValue, options, provider, mergedCtx);
|
||||
|
||||
details = FlagEvaluationDetails.from(providerEval, key);
|
||||
hookSupport.afterHooks(type, hookCtx, details, mergedHooks, hints);
|
||||
} catch (Exception e) {
|
||||
log.error("Unable to correctly evaluate flag with key {} due to exception {}", key, e.getMessage());
|
||||
if (details == null) {
|
||||
details = FlagEvaluationDetails.<T>builder().build();
|
||||
}
|
||||
details.setValue(defaultValue);
|
||||
details.setReason(Reason.ERROR);
|
||||
details.setErrorCode(e.getMessage());
|
||||
hookSupport.errorHooks(type, hookCtx, e, mergedHooks, hints);
|
||||
} finally {
|
||||
hookSupport.afterAllHooks(type, hookCtx, mergedHooks, hints);
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
private <T> ProviderEvaluation<?> createProviderEvaluation(
|
||||
FlagValueType type,
|
||||
String key,
|
||||
T defaultValue,
|
||||
FlagEvaluationOptions options,
|
||||
FeatureProvider provider,
|
||||
EvaluationContext invocationContext
|
||||
) {
|
||||
switch (type) {
|
||||
case BOOLEAN:
|
||||
return provider.getBooleanEvaluation(key, (Boolean) defaultValue, invocationContext, options);
|
||||
case STRING:
|
||||
return provider.getStringEvaluation(key, (String) defaultValue, invocationContext, options);
|
||||
case INTEGER:
|
||||
return provider.getIntegerEvaluation(key, (Integer) defaultValue, invocationContext, options);
|
||||
case DOUBLE:
|
||||
return provider.getDoubleEvaluation(key, (Double) defaultValue, invocationContext, options);
|
||||
case OBJECT:
|
||||
return provider.getObjectEvaluation(key, defaultValue, invocationContext, options);
|
||||
default:
|
||||
throw new GeneralError("Unknown flag type");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getBooleanValue(String key, Boolean defaultValue) {
|
||||
return getBooleanDetails(key, defaultValue).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationContext ctx) {
|
||||
return getBooleanDetails(key, defaultValue, ctx).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
return getBooleanDetails(key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue) {
|
||||
return getBooleanDetails(key, defaultValue, new EvaluationContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx) {
|
||||
return getBooleanDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.BOOLEAN, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStringValue(String key, String defaultValue) {
|
||||
return getStringDetails(key, defaultValue).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStringValue(String key, String defaultValue, EvaluationContext ctx) {
|
||||
return getStringDetails(key, defaultValue, ctx).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStringValue(String key, String defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
return getStringDetails(key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue) {
|
||||
return getStringDetails(key, defaultValue, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx) {
|
||||
return getStringDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.STRING, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getIntegerValue(String key, Integer defaultValue) {
|
||||
return getIntegerDetails(key, defaultValue).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getIntegerValue(String key, Integer defaultValue, EvaluationContext ctx) {
|
||||
return getIntegerDetails(key, defaultValue, ctx).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getIntegerValue(String key, Integer defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
return getIntegerDetails(key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue) {
|
||||
return getIntegerDetails(key, defaultValue, new EvaluationContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx) {
|
||||
return getIntegerDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.INTEGER, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getDoubleValue(String key, Double defaultValue) {
|
||||
return getDoubleValue(key, defaultValue, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getDoubleValue(String key, Double defaultValue, EvaluationContext ctx) {
|
||||
return getDoubleValue(key, defaultValue, ctx, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getDoubleValue(String key, Double defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue) {
|
||||
return getDoubleDetails(key, defaultValue, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx) {
|
||||
return getDoubleDetails(key, defaultValue, ctx, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getObjectValue(String key, T defaultValue) {
|
||||
return getObjectDetails(key, defaultValue).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getObjectValue(String key, T defaultValue, EvaluationContext ctx) {
|
||||
return getObjectDetails(key, defaultValue, ctx).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T getObjectValue(String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return getObjectDetails(key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> FlagEvaluationDetails<T> getObjectDetails(String key, T defaultValue) {
|
||||
return getObjectDetails(key, defaultValue, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> FlagEvaluationDetails<T> getObjectDetails(String key, T defaultValue, EvaluationContext ctx) {
|
||||
return getObjectDetails(key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> FlagEvaluationDetails<T> getObjectDetails(String key, T defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.OBJECT, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Metadata getMetadata() {
|
||||
return () -> name;
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
@Data @Builder
|
||||
public class ProviderEvaluation<T> implements BaseEvaluation<T> {
|
||||
T value;
|
||||
@Nullable String variant;
|
||||
Reason reason;
|
||||
@Nullable String errorCode;
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
public enum Reason {
|
||||
DISABLED, SPLIT, TARGETING_MATCH, DEFAULT, UNKNOWN, ERROR
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package dev.openfeature.javasdk;
|
||||
|
||||
public interface StringHook extends Hook<String> {
|
||||
|
||||
@Override
|
||||
default boolean supportsFlagValueType(FlagValueType flagValueType) {
|
||||
return FlagValueType.STRING == flagValueType;
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
package dev.openfeature.javasdk.exceptions;
|
||||
|
||||
import dev.openfeature.javasdk.ErrorCode;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.StandardException;
|
||||
|
||||
@StandardException
|
||||
public class FlagNotFoundError extends OpenFeatureError {
|
||||
private static final long serialVersionUID = 1L;
|
||||
@Getter private final ErrorCode errorCode = ErrorCode.GENERAL;
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package dev.openfeature.javasdk.exceptions;
|
||||
|
||||
import dev.openfeature.javasdk.ErrorCode;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.StandardException;
|
||||
|
||||
@StandardException
|
||||
public class ParseError extends OpenFeatureError {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Getter private final ErrorCode errorCode = ErrorCode.PARSE_ERROR;
|
||||
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
package dev.openfeature.javasdk.exceptions;
|
||||
|
||||
import dev.openfeature.javasdk.ErrorCode;
|
||||
import lombok.Getter;
|
||||
import lombok.experimental.StandardException;
|
||||
|
||||
@StandardException
|
||||
public class TypeMismatchError extends OpenFeatureError {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Getter private final ErrorCode errorCode = ErrorCode.TYPE_MISMATCH;
|
||||
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
package dev.openfeature.javasdk.internal;
|
||||
|
||||
import lombok.Value;
|
||||
|
||||
@Value
|
||||
public class Pair<K,V> {
|
||||
private K first;
|
||||
private V second;
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
|
||||
@EqualsAndHashCode
|
||||
abstract class AbstractStructure implements Structure {
|
||||
|
||||
protected final Map<String, Value> attributes;
|
||||
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return attributes == null || attributes.isEmpty();
|
||||
}
|
||||
|
||||
AbstractStructure() {
|
||||
this.attributes = new HashMap<>();
|
||||
}
|
||||
|
||||
AbstractStructure(Map<String, Value> attributes) {
|
||||
this.attributes = attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an unmodifiable representation of the internal attribute map.
|
||||
*
|
||||
* @return immutable map
|
||||
*/
|
||||
public Map<String, Value> asUnmodifiableMap() {
|
||||
return Collections.unmodifiableMap(attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all values as their underlying primitives types.
|
||||
*
|
||||
* @return all attributes on the structure into a Map
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Object> asObjectMap() {
|
||||
return attributes.entrySet().stream()
|
||||
// custom collector, workaround for Collectors.toMap in JDK8
|
||||
// https://bugs.openjdk.org/browse/JDK-8148463
|
||||
.collect(
|
||||
HashMap::new,
|
||||
(accumulated, entry) -> accumulated.put(entry.getKey(), convertValue(entry.getValue())),
|
||||
HashMap::putAll);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* A class to help with synchronization by allowing the optional awaiting of the associated action.
|
||||
*/
|
||||
public class Awaitable {
|
||||
|
||||
/**
|
||||
* An already-completed Awaitable. Awaiting this will return immediately.
|
||||
*/
|
||||
public static final Awaitable FINISHED = new Awaitable(true);
|
||||
|
||||
private boolean isDone = false;
|
||||
|
||||
public Awaitable() {}
|
||||
|
||||
private Awaitable(boolean isDone) {
|
||||
this.isDone = isDone;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lets the calling thread wait until some other thread calls {@link Awaitable#wakeup()}. If
|
||||
* {@link Awaitable#wakeup()} has been called before the current thread invokes this method, it will return
|
||||
* immediately.
|
||||
*/
|
||||
@SuppressWarnings("java:S2142")
|
||||
public synchronized void await() {
|
||||
while (!isDone) {
|
||||
try {
|
||||
this.wait();
|
||||
} catch (InterruptedException ignored) {
|
||||
// ignored, do not propagate the interrupted state
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Wakes up all threads that have called {@link Awaitable#await()} and lets them proceed.
|
||||
*/
|
||||
public synchronized void wakeup() {
|
||||
isDone = true;
|
||||
this.notifyAll();
|
||||
}
|
||||
}
|
|
@ -1,31 +1,44 @@
|
|||
package dev.openfeature.javasdk;
|
||||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* This is a common interface between the evaluation results that providers return and what is given to the end users.
|
||||
*
|
||||
* @param <T> The type of flag being evaluated.
|
||||
*/
|
||||
public interface BaseEvaluation<T> {
|
||||
/**
|
||||
* Returns the resolved value of the evaluation.
|
||||
*
|
||||
* @return {T} the resolve value
|
||||
*/
|
||||
T getValue();
|
||||
|
||||
/**
|
||||
* Returns an identifier for this value, if applicable.
|
||||
*
|
||||
* @return {String} value identifier
|
||||
*/
|
||||
String getVariant();
|
||||
|
||||
/**
|
||||
* Describes how we came to the value that we're returning.
|
||||
*
|
||||
* @return {Reason}
|
||||
*/
|
||||
Reason getReason();
|
||||
String getReason();
|
||||
|
||||
/**
|
||||
* The error code, if applicable. Should only be set when the Reason is ERROR.
|
||||
*
|
||||
* @return {ErrorCode}
|
||||
*/
|
||||
String getErrorCode();
|
||||
ErrorCode getErrorCode();
|
||||
|
||||
/**
|
||||
* The error message (usually from exception.getMessage()), if applicable.
|
||||
* Should only be set when the Reason is ERROR.
|
||||
*
|
||||
* @return {String}
|
||||
*/
|
||||
String getErrorMessage();
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* An extension point which can run around flag resolution. They are intended to be used as a way to add custom logic
|
||||
* to the lifecycle of flag evaluation.
|
||||
*
|
||||
* @see Hook
|
||||
*/
|
||||
public interface BooleanHook extends Hook<Boolean> {
|
||||
|
||||
@Override
|
||||
default boolean supportsFlagValueType(FlagValueType flagValueType) {
|
||||
return FlagValueType.BOOLEAN == flagValueType;
|
||||
}
|
||||
}
|
|
@ -1,24 +1,26 @@
|
|||
package dev.openfeature.javasdk;
|
||||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Interface used to resolve flags of varying types.
|
||||
*/
|
||||
public interface Client extends Features {
|
||||
Metadata getMetadata();
|
||||
public interface Client extends Features, Tracking, EventBus<Client> {
|
||||
ClientMetadata getMetadata();
|
||||
|
||||
/**
|
||||
* Return an optional client-level evaluation context.
|
||||
*
|
||||
* @return {@link EvaluationContext}
|
||||
*/
|
||||
EvaluationContext getEvaluationContext();
|
||||
|
||||
/**
|
||||
* Set the client-level evaluation context.
|
||||
*
|
||||
* @param ctx Client level context.
|
||||
*/
|
||||
void setEvaluationContext(EvaluationContext ctx);
|
||||
Client setEvaluationContext(EvaluationContext ctx);
|
||||
|
||||
/**
|
||||
* Adds hooks for evaluation.
|
||||
|
@ -26,11 +28,19 @@ public interface Client extends Features {
|
|||
*
|
||||
* @param hooks The hook to add.
|
||||
*/
|
||||
void addHooks(Hook... hooks);
|
||||
Client addHooks(Hook... hooks);
|
||||
|
||||
/**
|
||||
* Fetch the hooks associated to this client.
|
||||
*
|
||||
* @return A list of {@link Hook}s.
|
||||
*/
|
||||
List<Hook> getClientHooks();
|
||||
List<Hook> getHooks();
|
||||
|
||||
/**
|
||||
* Returns the current state of the associated provider.
|
||||
*
|
||||
* @return the provider state
|
||||
*/
|
||||
ProviderState getProviderState();
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* Metadata specific to an OpenFeature {@code Client}.
|
||||
*/
|
||||
public interface ClientMetadata {
|
||||
String getDomain();
|
||||
|
||||
@Deprecated
|
||||
// this is here for compatibility with getName() exposed from {@link Metadata}
|
||||
default String getName() {
|
||||
return getDomain();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* An extension point which can run around flag resolution. They are intended to be used as a way to add custom logic
|
||||
* to the lifecycle of flag evaluation.
|
||||
*
|
||||
* @see Hook
|
||||
*/
|
||||
public interface DoubleHook extends Hook<Double> {
|
||||
|
||||
@Override
|
||||
default boolean supportsFlagValueType(FlagValueType flagValueType) {
|
||||
return FlagValueType.DOUBLE == flagValueType;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
@SuppressWarnings("checkstyle:MissingJavadocType")
|
||||
public enum ErrorCode {
|
||||
PROVIDER_NOT_READY,
|
||||
FLAG_NOT_FOUND,
|
||||
PARSE_ERROR,
|
||||
TYPE_MISMATCH,
|
||||
TARGETING_KEY_MISSING,
|
||||
INVALID_CONTEXT,
|
||||
GENERAL,
|
||||
PROVIDER_FATAL
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.function.Function;
|
||||
|
||||
/**
|
||||
* The EvaluationContext is a container for arbitrary contextual data
|
||||
* that can be used as a basis for dynamic evaluation.
|
||||
*/
|
||||
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
|
||||
public interface EvaluationContext extends Structure {
|
||||
|
||||
String TARGETING_KEY = "targetingKey";
|
||||
|
||||
String getTargetingKey();
|
||||
|
||||
/**
|
||||
* Merges this EvaluationContext object with the second overriding the this in
|
||||
* case of conflict.
|
||||
*
|
||||
* @param overridingContext overriding context
|
||||
* @return resulting merged context
|
||||
*/
|
||||
EvaluationContext merge(EvaluationContext overridingContext);
|
||||
|
||||
/**
|
||||
* Recursively merges the overriding map into the base Value map.
|
||||
* The base map is mutated, the overriding map is not.
|
||||
* Null maps will cause no-op.
|
||||
*
|
||||
* @param newStructure function to create the right structure(s) for Values
|
||||
* @param base base map to merge
|
||||
* @param overriding overriding map to merge
|
||||
*/
|
||||
static void mergeMaps(
|
||||
Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
|
||||
if (base == null) {
|
||||
return;
|
||||
}
|
||||
if (overriding == null || overriding.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Entry<String, Value> overridingEntry : overriding.entrySet()) {
|
||||
String key = overridingEntry.getKey();
|
||||
if (overridingEntry.getValue().isStructure()
|
||||
&& base.containsKey(key)
|
||||
&& base.get(key).isStructure()) {
|
||||
Structure mergedValue = base.get(key).asStructure();
|
||||
Structure overridingValue = overridingEntry.getValue().asStructure();
|
||||
Map<String, Value> newMap = mergedValue.asMap();
|
||||
mergeMaps(newStructure, newMap, overridingValue.asUnmodifiableMap());
|
||||
base.put(key, new Value(newStructure.apply(newMap)));
|
||||
} else {
|
||||
base.put(key, overridingEntry.getValue());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.Builder;
|
||||
import lombok.Getter;
|
||||
import lombok.Singular;
|
||||
|
||||
/**
|
||||
* Represents an evaluation event.
|
||||
*/
|
||||
@Builder
|
||||
@Getter
|
||||
public class EvaluationEvent {
|
||||
|
||||
private String name;
|
||||
|
||||
@Singular("attribute")
|
||||
private Map<String, Object> attributes;
|
||||
|
||||
public Map<String, Object> getAttributes() {
|
||||
return new HashMap<>(attributes);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.function.Consumer;
|
||||
|
||||
/**
|
||||
* Interface for attaching event handlers.
|
||||
*/
|
||||
public interface EventBus<T> {
|
||||
|
||||
/**
|
||||
* Add a handler for the {@link ProviderEvent#PROVIDER_READY} event.
|
||||
* Shorthand for {@link #on(ProviderEvent, Consumer)}
|
||||
*
|
||||
* @param handler behavior to add with this event
|
||||
* @return this
|
||||
*/
|
||||
T onProviderReady(Consumer<EventDetails> handler);
|
||||
|
||||
/**
|
||||
* Add a handler for the {@link ProviderEvent#PROVIDER_CONFIGURATION_CHANGED} event.
|
||||
* Shorthand for {@link #on(ProviderEvent, Consumer)}
|
||||
*
|
||||
* @param handler behavior to add with this event
|
||||
* @return this
|
||||
*/
|
||||
T onProviderConfigurationChanged(Consumer<EventDetails> handler);
|
||||
|
||||
/**
|
||||
* Add a handler for the {@link ProviderEvent#PROVIDER_STALE} event.
|
||||
* Shorthand for {@link #on(ProviderEvent, Consumer)}
|
||||
*
|
||||
* @param handler behavior to add with this event
|
||||
* @return this
|
||||
*/
|
||||
T onProviderError(Consumer<EventDetails> handler);
|
||||
|
||||
/**
|
||||
* Add a handler for the {@link ProviderEvent#PROVIDER_ERROR} event.
|
||||
* Shorthand for {@link #on(ProviderEvent, Consumer)}
|
||||
*
|
||||
* @param handler behavior to add with this event
|
||||
* @return this
|
||||
*/
|
||||
T onProviderStale(Consumer<EventDetails> handler);
|
||||
|
||||
/**
|
||||
* Add a handler for the specified {@link ProviderEvent}.
|
||||
*
|
||||
* @param event event type
|
||||
* @param handler behavior to add with this event
|
||||
* @return this
|
||||
*/
|
||||
T on(ProviderEvent event, Consumer<EventDetails> handler);
|
||||
|
||||
/**
|
||||
* Remove the previously attached handler by reference.
|
||||
* If the handler doesn't exists, no-op.
|
||||
*
|
||||
* @param event event type
|
||||
* @param handler to be removed
|
||||
* @return this
|
||||
*/
|
||||
T removeHandler(ProviderEvent event, Consumer<EventDetails> handler);
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
/**
|
||||
* The details of a particular event.
|
||||
*/
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@SuperBuilder(toBuilder = true)
|
||||
public class EventDetails extends ProviderEventDetails {
|
||||
private String domain;
|
||||
private String providerName;
|
||||
|
||||
static EventDetails fromProviderEventDetails(ProviderEventDetails providerEventDetails, String providerName) {
|
||||
return fromProviderEventDetails(providerEventDetails, providerName, null);
|
||||
}
|
||||
|
||||
static EventDetails fromProviderEventDetails(
|
||||
ProviderEventDetails providerEventDetails, String providerName, String domain) {
|
||||
return builder()
|
||||
.domain(domain)
|
||||
.providerName(providerName)
|
||||
.flagsChanged(providerEventDetails.getFlagsChanged())
|
||||
.eventMetadata(providerEventDetails.getEventMetadata())
|
||||
.message(providerEventDetails.getMessage())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,147 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.internal.TriConsumer;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Abstract EventProvider. Providers must extend this class to support events.
|
||||
* Emit events with {@link #emit(ProviderEvent, ProviderEventDetails)}. Please
|
||||
* note that the SDK will automatically emit
|
||||
* {@link ProviderEvent#PROVIDER_READY } or
|
||||
* {@link ProviderEvent#PROVIDER_ERROR } accordingly when
|
||||
* {@link FeatureProvider#initialize(EvaluationContext)} completes successfully
|
||||
* or with error, so these events need not be emitted manually during
|
||||
* initialization.
|
||||
*
|
||||
* @see FeatureProvider
|
||||
*/
|
||||
@Slf4j
|
||||
public abstract class EventProvider implements FeatureProvider {
|
||||
private EventProviderListener eventProviderListener;
|
||||
private final ExecutorService emitterExecutor = Executors.newCachedThreadPool();
|
||||
|
||||
void setEventProviderListener(EventProviderListener eventProviderListener) {
|
||||
this.eventProviderListener = eventProviderListener;
|
||||
}
|
||||
|
||||
private TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails> onEmit = null;
|
||||
|
||||
/**
|
||||
* "Attach" this EventProvider to an SDK, which allows events to propagate from this provider.
|
||||
* No-op if the same onEmit is already attached.
|
||||
*
|
||||
* @param onEmit the function to run when a provider emits events.
|
||||
* @throws IllegalStateException if attempted to bind a new emitter for already bound provider
|
||||
*/
|
||||
void attach(TriConsumer<EventProvider, ProviderEvent, ProviderEventDetails> onEmit) {
|
||||
if (this.onEmit != null && this.onEmit != onEmit) {
|
||||
// if we are trying to attach this provider to a different onEmit, something has gone wrong
|
||||
throw new IllegalStateException("Provider " + this.getMetadata().getName() + " is already attached.");
|
||||
} else {
|
||||
this.onEmit = onEmit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* "Detach" this EventProvider from an SDK, stopping propagation of all events.
|
||||
*/
|
||||
void detach() {
|
||||
this.onEmit = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the event emitter executor and block until either termination has completed
|
||||
* or timeout period has elapsed.
|
||||
*/
|
||||
@Override
|
||||
public void shutdown() {
|
||||
emitterExecutor.shutdown();
|
||||
try {
|
||||
if (!emitterExecutor.awaitTermination(EventSupport.SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
log.warn("Emitter executor did not terminate before the timeout period had elapsed");
|
||||
emitterExecutor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
emitterExecutor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit the specified {@link ProviderEvent}.
|
||||
*
|
||||
* @param event The event type
|
||||
* @param details The details of the event
|
||||
*/
|
||||
public Awaitable emit(final ProviderEvent event, final ProviderEventDetails details) {
|
||||
final var localEventProviderListener = this.eventProviderListener;
|
||||
final var localOnEmit = this.onEmit;
|
||||
|
||||
if (localEventProviderListener == null && localOnEmit == null) {
|
||||
return Awaitable.FINISHED;
|
||||
}
|
||||
|
||||
final var awaitable = new Awaitable();
|
||||
|
||||
// These calls need to be executed on a different thread to prevent deadlocks when the provider initialization
|
||||
// relies on a ready event to be emitted
|
||||
emitterExecutor.submit(() -> {
|
||||
try (var ignored = OpenFeatureAPI.lock.readLockAutoCloseable()) {
|
||||
if (localEventProviderListener != null) {
|
||||
localEventProviderListener.onEmit(event, details);
|
||||
}
|
||||
if (localOnEmit != null) {
|
||||
localOnEmit.accept(this, event, details);
|
||||
}
|
||||
} finally {
|
||||
awaitable.wakeup();
|
||||
}
|
||||
});
|
||||
|
||||
return awaitable;
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a {@link ProviderEvent#PROVIDER_READY} event.
|
||||
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
|
||||
*
|
||||
* @param details The details of the event
|
||||
*/
|
||||
public Awaitable emitProviderReady(ProviderEventDetails details) {
|
||||
return emit(ProviderEvent.PROVIDER_READY, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a
|
||||
* {@link ProviderEvent#PROVIDER_CONFIGURATION_CHANGED}
|
||||
* event. Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
|
||||
*
|
||||
* @param details The details of the event
|
||||
*/
|
||||
public Awaitable emitProviderConfigurationChanged(ProviderEventDetails details) {
|
||||
return emit(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a {@link ProviderEvent#PROVIDER_STALE} event.
|
||||
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
|
||||
*
|
||||
* @param details The details of the event
|
||||
*/
|
||||
public Awaitable emitProviderStale(ProviderEventDetails details) {
|
||||
return emit(ProviderEvent.PROVIDER_STALE, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a {@link ProviderEvent#PROVIDER_ERROR} event.
|
||||
* Shorthand for {@link #emit(ProviderEvent, ProviderEventDetails)}
|
||||
*
|
||||
* @param details The details of the event
|
||||
*/
|
||||
public Awaitable emitProviderError(ProviderEventDetails details) {
|
||||
return emit(ProviderEvent.PROVIDER_ERROR, details);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
@FunctionalInterface
|
||||
interface EventProviderListener {
|
||||
void onEmit(ProviderEvent event, ProviderEventDetails details);
|
||||
}
|
|
@ -0,0 +1,177 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Util class for storing and running handlers.
|
||||
*/
|
||||
@Slf4j
|
||||
class EventSupport {
|
||||
|
||||
public static final int SHUTDOWN_TIMEOUT_SECONDS = 3;
|
||||
|
||||
// we use a v4 uuid as a "placeholder" for anonymous clients, since
|
||||
// ConcurrentHashMap doesn't support nulls
|
||||
private static final String DEFAULT_CLIENT_UUID = UUID.randomUUID().toString();
|
||||
private final Map<String, HandlerStore> handlerStores = new ConcurrentHashMap<>();
|
||||
private final HandlerStore globalHandlerStore = new HandlerStore();
|
||||
private final ExecutorService taskExecutor = Executors.newCachedThreadPool();
|
||||
|
||||
/**
|
||||
* Run all the event handlers associated with this domain.
|
||||
* If the domain is null, handlers attached to unnamed clients will run.
|
||||
*
|
||||
* @param domain the domain to run event handlers for, or null
|
||||
* @param event the event type
|
||||
* @param eventDetails the event details
|
||||
*/
|
||||
public void runClientHandlers(String domain, ProviderEvent event, EventDetails eventDetails) {
|
||||
domain = Optional.ofNullable(domain).orElse(DEFAULT_CLIENT_UUID);
|
||||
|
||||
// run handlers if they exist
|
||||
Optional.ofNullable(handlerStores.get(domain))
|
||||
.map(store -> store.handlerMap.get(event))
|
||||
.ifPresent(handlers -> handlers.forEach(handler -> runHandler(handler, eventDetails)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Run all the API (global) event handlers.
|
||||
*
|
||||
* @param event the event type
|
||||
* @param eventDetails the event details
|
||||
*/
|
||||
public void runGlobalHandlers(ProviderEvent event, EventDetails eventDetails) {
|
||||
globalHandlerStore.handlerMap.get(event).forEach(handler -> {
|
||||
runHandler(handler, eventDetails);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a handler for the specified domain, or all unnamed clients.
|
||||
*
|
||||
* @param domain the domain to add handlers for, or else unnamed
|
||||
* @param event the event type
|
||||
* @param handler the handler function to run
|
||||
*/
|
||||
public void addClientHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
final String name = Optional.ofNullable(domain).orElse(DEFAULT_CLIENT_UUID);
|
||||
|
||||
// lazily create and cache a HandlerStore if it doesn't exist
|
||||
HandlerStore store = Optional.ofNullable(this.handlerStores.get(name)).orElseGet(() -> {
|
||||
HandlerStore newStore = new HandlerStore();
|
||||
this.handlerStores.put(name, newStore);
|
||||
return newStore;
|
||||
});
|
||||
store.addHandler(event, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a client event handler for the specified event type.
|
||||
*
|
||||
* @param domain the domain of the client handler to remove, or null to remove
|
||||
* from unnamed clients
|
||||
* @param event the event type
|
||||
* @param handler the handler ref to be removed
|
||||
*/
|
||||
public void removeClientHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
domain = Optional.ofNullable(domain).orElse(DEFAULT_CLIENT_UUID);
|
||||
this.handlerStores.get(domain).removeHandler(event, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a global event handler of the specified event type.
|
||||
*
|
||||
* @param event the event type
|
||||
* @param handler the handler to be added
|
||||
*/
|
||||
public void addGlobalHandler(ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
this.globalHandlerStore.addHandler(event, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a global event handler for the specified event type.
|
||||
*
|
||||
* @param event the event type
|
||||
* @param handler the handler ref to be removed
|
||||
*/
|
||||
public void removeGlobalHandler(ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
this.globalHandlerStore.removeHandler(event, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all domain names for which we have event handlers registered.
|
||||
*
|
||||
* @return set of domain names
|
||||
*/
|
||||
public Set<String> getAllDomainNames() {
|
||||
return this.handlerStores.keySet();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run the passed handler on the taskExecutor.
|
||||
*
|
||||
* @param handler the handler to run
|
||||
* @param eventDetails the event details
|
||||
*/
|
||||
public void runHandler(Consumer<EventDetails> handler, EventDetails eventDetails) {
|
||||
taskExecutor.submit(() -> {
|
||||
try {
|
||||
handler.accept(eventDetails);
|
||||
} catch (Exception e) {
|
||||
log.error("Exception in event handler {}", handler, e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the event handler task executor and block until either termination has completed
|
||||
* or timeout period has elapsed.
|
||||
*/
|
||||
public void shutdown() {
|
||||
taskExecutor.shutdown();
|
||||
try {
|
||||
if (!taskExecutor.awaitTermination(SHUTDOWN_TIMEOUT_SECONDS, TimeUnit.SECONDS)) {
|
||||
log.warn("Task executor did not terminate before the timeout period had elapsed");
|
||||
taskExecutor.shutdownNow();
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
taskExecutor.shutdownNow();
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
|
||||
// Handler store maintains a set of handlers for each event type.
|
||||
// Each client in the SDK gets it's own handler store, which is lazily
|
||||
// instantiated when a handler is added to that client.
|
||||
static class HandlerStore {
|
||||
|
||||
private final Map<ProviderEvent, Collection<Consumer<EventDetails>>> handlerMap;
|
||||
|
||||
HandlerStore() {
|
||||
handlerMap = new ConcurrentHashMap<>();
|
||||
handlerMap.put(ProviderEvent.PROVIDER_READY, new ConcurrentLinkedQueue<>());
|
||||
handlerMap.put(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, new ConcurrentLinkedQueue<>());
|
||||
handlerMap.put(ProviderEvent.PROVIDER_ERROR, new ConcurrentLinkedQueue<>());
|
||||
handlerMap.put(ProviderEvent.PROVIDER_STALE, new ConcurrentLinkedQueue<>());
|
||||
}
|
||||
|
||||
void addHandler(ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
handlerMap.get(event).add(handler);
|
||||
}
|
||||
|
||||
void removeHandler(ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
handlerMap.get(event).remove(handler);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,84 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* The interface implemented by upstream flag providers to resolve flags for
|
||||
* their service. If you want to support realtime events with your provider, you
|
||||
* should extend {@link EventProvider}
|
||||
*/
|
||||
public interface FeatureProvider {
|
||||
Metadata getMetadata();
|
||||
|
||||
default List<Hook> getProviderHooks() {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
|
||||
ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx);
|
||||
|
||||
ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx);
|
||||
|
||||
ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx);
|
||||
|
||||
ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx);
|
||||
|
||||
ProviderEvaluation<Value> getObjectEvaluation(String key, Value defaultValue, EvaluationContext ctx);
|
||||
|
||||
/**
|
||||
* This method is called before a provider is used to evaluate flags. Providers
|
||||
* can overwrite this method,
|
||||
* if they have special initialization needed prior being called for flag
|
||||
* evaluation.
|
||||
*
|
||||
* <p>
|
||||
* It is ok if the method is expensive as it is executed in the background. All
|
||||
* runtime exceptions will be
|
||||
* caught and logged.
|
||||
* </p>
|
||||
*/
|
||||
default void initialize(EvaluationContext evaluationContext) throws Exception {
|
||||
// Intentionally left blank
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is called when a new provider is about to be used to evaluate
|
||||
* flags, or the SDK is shut down.
|
||||
* Providers can overwrite this method, if they have special shutdown actions
|
||||
* needed.
|
||||
*
|
||||
* <p>
|
||||
* It is ok if the method is expensive as it is executed in the background. All
|
||||
* runtime exceptions will be
|
||||
* caught and logged.
|
||||
* </p>
|
||||
*/
|
||||
default void shutdown() {
|
||||
// Intentionally left blank
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a representation of the current readiness of the provider.
|
||||
* If the provider needs to be initialized, it should return {@link ProviderState#NOT_READY}.
|
||||
* If the provider is in an error state, it should return {@link ProviderState#ERROR}.
|
||||
* If the provider is functioning normally, it should return {@link ProviderState#READY}.
|
||||
*
|
||||
* <p><i>Providers which do not implement this method are assumed to be ready immediately.</i></p>
|
||||
*
|
||||
* @return ProviderState
|
||||
* @deprecated The state is handled by the SDK internally. Query the state from the {@link Client} instead.
|
||||
*/
|
||||
@Deprecated
|
||||
default ProviderState getState() {
|
||||
return ProviderState.READY;
|
||||
}
|
||||
|
||||
/**
|
||||
* Feature provider implementations can opt in for to support Tracking by implementing this method.
|
||||
*
|
||||
* @param eventName The name of the tracking event
|
||||
* @param context Evaluation context used in flag evaluation (Optional)
|
||||
* @param details Data pertinent to a particular tracking event (Optional)
|
||||
*/
|
||||
default void track(String eventName, EvaluationContext context, TrackingEventDetails details) {}
|
||||
}
|
|
@ -0,0 +1,88 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.OpenFeatureError;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
class FeatureProviderStateManager implements EventProviderListener {
|
||||
private final FeatureProvider delegate;
|
||||
private final AtomicBoolean isInitialized = new AtomicBoolean();
|
||||
private final AtomicReference<ProviderState> state = new AtomicReference<>(ProviderState.NOT_READY);
|
||||
|
||||
public FeatureProviderStateManager(FeatureProvider delegate) {
|
||||
this.delegate = delegate;
|
||||
if (delegate instanceof EventProvider) {
|
||||
((EventProvider) delegate).setEventProviderListener(this);
|
||||
}
|
||||
}
|
||||
|
||||
public void initialize(EvaluationContext evaluationContext) throws Exception {
|
||||
if (isInitialized.getAndSet(true)) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
delegate.initialize(evaluationContext);
|
||||
setState(ProviderState.READY);
|
||||
} catch (OpenFeatureError openFeatureError) {
|
||||
if (ErrorCode.PROVIDER_FATAL.equals(openFeatureError.getErrorCode())) {
|
||||
setState(ProviderState.FATAL);
|
||||
} else {
|
||||
setState(ProviderState.ERROR);
|
||||
}
|
||||
isInitialized.set(false);
|
||||
throw openFeatureError;
|
||||
} catch (Exception e) {
|
||||
setState(ProviderState.ERROR);
|
||||
isInitialized.set(false);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
delegate.shutdown();
|
||||
setState(ProviderState.NOT_READY);
|
||||
isInitialized.set(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEmit(ProviderEvent event, ProviderEventDetails details) {
|
||||
if (ProviderEvent.PROVIDER_ERROR.equals(event)) {
|
||||
if (details != null && details.getErrorCode() == ErrorCode.PROVIDER_FATAL) {
|
||||
setState(ProviderState.FATAL);
|
||||
} else {
|
||||
setState(ProviderState.ERROR);
|
||||
}
|
||||
} else if (ProviderEvent.PROVIDER_STALE.equals(event)) {
|
||||
setState(ProviderState.STALE);
|
||||
} else if (ProviderEvent.PROVIDER_READY.equals(event)) {
|
||||
setState(ProviderState.READY);
|
||||
}
|
||||
}
|
||||
|
||||
private void setState(ProviderState state) {
|
||||
ProviderState oldState = this.state.getAndSet(state);
|
||||
if (oldState != state) {
|
||||
String providerName;
|
||||
if (delegate.getMetadata() == null || delegate.getMetadata().getName() == null) {
|
||||
providerName = "unknown";
|
||||
} else {
|
||||
providerName = delegate.getMetadata().getName();
|
||||
}
|
||||
log.info("Provider {} transitioned from state {} to state {}", providerName, oldState, state);
|
||||
}
|
||||
}
|
||||
|
||||
public ProviderState getState() {
|
||||
return state.get();
|
||||
}
|
||||
|
||||
FeatureProvider getProvider() {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
public boolean hasSameProvider(FeatureProvider featureProvider) {
|
||||
return this.delegate.equals(featureProvider);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package dev.openfeature.javasdk;
|
||||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* An API for the type-specific fetch methods offered to users.
|
||||
|
@ -15,8 +15,8 @@ public interface Features {
|
|||
|
||||
FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx);
|
||||
|
||||
FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
FlagEvaluationDetails<Boolean> getBooleanDetails(
|
||||
String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
|
||||
String getStringValue(String key, String defaultValue);
|
||||
|
||||
|
@ -28,8 +28,8 @@ public interface Features {
|
|||
|
||||
FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx);
|
||||
|
||||
FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
FlagEvaluationDetails<String> getStringDetails(
|
||||
String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
|
||||
Integer getIntegerValue(String key, Integer defaultValue);
|
||||
|
||||
|
@ -41,8 +41,8 @@ public interface Features {
|
|||
|
||||
FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx);
|
||||
|
||||
FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
FlagEvaluationDetails<Integer> getIntegerDetails(
|
||||
String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
|
||||
Double getDoubleValue(String key, Double defaultValue);
|
||||
|
||||
|
@ -54,19 +54,19 @@ public interface Features {
|
|||
|
||||
FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx);
|
||||
|
||||
FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
FlagEvaluationDetails<Double> getDoubleDetails(
|
||||
String key, Double defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
|
||||
<T> T getObjectValue(String key, T defaultValue);
|
||||
Value getObjectValue(String key, Value defaultValue);
|
||||
|
||||
<T> T getObjectValue(String key, T defaultValue, EvaluationContext ctx);
|
||||
Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx);
|
||||
|
||||
<T> T getObjectValue(String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
|
||||
<T> FlagEvaluationDetails<T> getObjectDetails(String key, T defaultValue);
|
||||
FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue);
|
||||
|
||||
<T> FlagEvaluationDetails<T> getObjectDetails(String key, T defaultValue, EvaluationContext ctx);
|
||||
FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue, EvaluationContext ctx);
|
||||
|
||||
<T> FlagEvaluationDetails<T> getObjectDetails(String key, T defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options);
|
||||
FlagEvaluationDetails<Value> getObjectDetails(
|
||||
String key, Value defaultValue, EvaluationContext ctx, FlagEvaluationOptions options);
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.Optional;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Contains information about how the provider resolved a flag, including the
|
||||
* resolved value.
|
||||
*
|
||||
* @param <T> the type of the flag being evaluated.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class FlagEvaluationDetails<T> implements BaseEvaluation<T> {
|
||||
|
||||
private String flagKey;
|
||||
private T value;
|
||||
private String variant;
|
||||
private String reason;
|
||||
private ErrorCode errorCode;
|
||||
private String errorMessage;
|
||||
|
||||
@Builder.Default
|
||||
private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();
|
||||
|
||||
/**
|
||||
* Generate detail payload from the provider response.
|
||||
*
|
||||
* @param providerEval provider response
|
||||
* @param flagKey key for the flag being evaluated
|
||||
* @param <T> type of flag being returned
|
||||
* @return detail payload
|
||||
*/
|
||||
public static <T> FlagEvaluationDetails<T> from(ProviderEvaluation<T> providerEval, String flagKey) {
|
||||
return FlagEvaluationDetails.<T>builder()
|
||||
.flagKey(flagKey)
|
||||
.value(providerEval.getValue())
|
||||
.variant(providerEval.getVariant())
|
||||
.reason(providerEval.getReason())
|
||||
.errorMessage(providerEval.getErrorMessage())
|
||||
.errorCode(providerEval.getErrorCode())
|
||||
.flagMetadata(Optional.ofNullable(providerEval.getFlagMetadata())
|
||||
.orElse(ImmutableMetadata.builder().build()))
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.Builder;
|
||||
import lombok.Singular;
|
||||
|
||||
@SuppressWarnings("checkstyle:MissingJavadocType")
|
||||
@lombok.Value
|
||||
@Builder
|
||||
public class FlagEvaluationOptions {
|
||||
@Singular
|
||||
List<Hook> hooks;
|
||||
|
||||
@Builder.Default
|
||||
Map<String, Object> hookHints = new HashMap<>();
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
@SuppressWarnings("checkstyle:MissingJavadocType")
|
||||
public enum FlagValueType {
|
||||
STRING,
|
||||
INTEGER,
|
||||
DOUBLE,
|
||||
OBJECT,
|
||||
BOOLEAN;
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package dev.openfeature.javasdk;
|
||||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
@ -16,7 +16,7 @@ public interface Hook<T> {
|
|||
* @param ctx Information about the particular flag evaluation
|
||||
* @param hints An immutable mapping of data for users to communicate to the hooks.
|
||||
* @return An optional {@link EvaluationContext}. If returned, it will be merged with the EvaluationContext
|
||||
* instances from other hooks, the client and API.
|
||||
* instances from other hooks, the client and API.
|
||||
*/
|
||||
default Optional<EvaluationContext> before(HookContext<T> ctx, Map<String, Object> hints) {
|
||||
return Optional.empty();
|
||||
|
@ -29,8 +29,7 @@ public interface Hook<T> {
|
|||
* @param details Information about how the flag was resolved, including any resolved values.
|
||||
* @param hints An immutable mapping of data for users to communicate to the hooks.
|
||||
*/
|
||||
default void after(HookContext<T> ctx, FlagEvaluationDetails<T> details, Map<String, Object> hints) {
|
||||
}
|
||||
default void after(HookContext<T> ctx, FlagEvaluationDetails<T> details, Map<String, Object> hints) {}
|
||||
|
||||
/**
|
||||
* Run when evaluation encounters an error. This will always run. Errors thrown will be swallowed.
|
||||
|
@ -39,8 +38,7 @@ public interface Hook<T> {
|
|||
* @param error The exception that was thrown.
|
||||
* @param hints An immutable mapping of data for users to communicate to the hooks.
|
||||
*/
|
||||
default void error(HookContext<T> ctx, Exception error, Map<String, Object> hints) {
|
||||
}
|
||||
default void error(HookContext<T> ctx, Exception error, Map<String, Object> hints) {}
|
||||
|
||||
/**
|
||||
* Run after flag evaluation, including any error processing. This will always run. Errors will be swallowed.
|
||||
|
@ -48,8 +46,7 @@ public interface Hook<T> {
|
|||
* @param ctx Information about the particular flag evaluation
|
||||
* @param hints An immutable mapping of data for users to communicate to the hooks.
|
||||
*/
|
||||
default void finallyAfter(HookContext<T> ctx, Map<String, Object> hints) {
|
||||
}
|
||||
default void finallyAfter(HookContext<T> ctx, FlagEvaluationDetails<T> details, Map<String, Object> hints) {}
|
||||
|
||||
default boolean supportsFlagValueType(FlagValueType flagValueType) {
|
||||
return true;
|
|
@ -1,4 +1,4 @@
|
|||
package dev.openfeature.javasdk;
|
||||
package dev.openfeature.sdk;
|
||||
|
||||
import lombok.Builder;
|
||||
import lombok.NonNull;
|
||||
|
@ -10,28 +10,40 @@ import lombok.With;
|
|||
*
|
||||
* @param <T> the type for the flag being evaluated
|
||||
*/
|
||||
@Value @Builder @With
|
||||
@Value
|
||||
@Builder
|
||||
@With
|
||||
public class HookContext<T> {
|
||||
@NonNull String flagKey;
|
||||
|
||||
@NonNull FlagValueType type;
|
||||
|
||||
@NonNull T defaultValue;
|
||||
|
||||
@NonNull EvaluationContext ctx;
|
||||
Metadata clientMetadata;
|
||||
|
||||
ClientMetadata clientMetadata;
|
||||
Metadata providerMetadata;
|
||||
|
||||
/**
|
||||
* Builds a {@link HookContext} instances from request data.
|
||||
* @param key feature flag key
|
||||
* @param type flag value type
|
||||
* @param clientMetadata info on which client is calling
|
||||
*
|
||||
* @param key feature flag key
|
||||
* @param type flag value type
|
||||
* @param clientMetadata info on which client is calling
|
||||
* @param providerMetadata info on the provider
|
||||
* @param ctx Evaluation Context for the request
|
||||
* @param defaultValue Fallback value
|
||||
* @param <T> type that the flag is evaluating against
|
||||
* @param ctx Evaluation Context for the request
|
||||
* @param defaultValue Fallback value
|
||||
* @param <T> type that the flag is evaluating against
|
||||
* @return resulting context for hook
|
||||
*/
|
||||
public static <T> HookContext<T> from(String key, FlagValueType type, Metadata clientMetadata,
|
||||
Metadata providerMetadata, EvaluationContext ctx, T defaultValue) {
|
||||
public static <T> HookContext<T> from(
|
||||
String key,
|
||||
FlagValueType type,
|
||||
ClientMetadata clientMetadata,
|
||||
Metadata providerMetadata,
|
||||
EvaluationContext ctx,
|
||||
T defaultValue) {
|
||||
return HookContext.<T>builder()
|
||||
.flagKey(key)
|
||||
.type(type)
|
|
@ -0,0 +1,101 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@SuppressWarnings({"unchecked", "rawtypes"})
|
||||
class HookSupport {
|
||||
|
||||
public EvaluationContext beforeHooks(
|
||||
FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks, Map<String, Object> hints) {
|
||||
return callBeforeHooks(flagValueType, hookCtx, hooks, hints);
|
||||
}
|
||||
|
||||
public void afterHooks(
|
||||
FlagValueType flagValueType,
|
||||
HookContext hookContext,
|
||||
FlagEvaluationDetails details,
|
||||
List<Hook> hooks,
|
||||
Map<String, Object> hints) {
|
||||
executeHooksUnchecked(flagValueType, hooks, hook -> hook.after(hookContext, details, hints));
|
||||
}
|
||||
|
||||
public void afterAllHooks(
|
||||
FlagValueType flagValueType,
|
||||
HookContext hookCtx,
|
||||
FlagEvaluationDetails details,
|
||||
List<Hook> hooks,
|
||||
Map<String, Object> hints) {
|
||||
executeHooks(flagValueType, hooks, "finally", hook -> hook.finallyAfter(hookCtx, details, hints));
|
||||
}
|
||||
|
||||
public void errorHooks(
|
||||
FlagValueType flagValueType,
|
||||
HookContext hookCtx,
|
||||
Exception e,
|
||||
List<Hook> hooks,
|
||||
Map<String, Object> hints) {
|
||||
executeHooks(flagValueType, hooks, "error", hook -> hook.error(hookCtx, e, hints));
|
||||
}
|
||||
|
||||
private <T> void executeHooks(
|
||||
FlagValueType flagValueType, List<Hook> hooks, String hookMethod, Consumer<Hook<T>> hookCode) {
|
||||
if (hooks != null) {
|
||||
for (Hook hook : hooks) {
|
||||
if (hook.supportsFlagValueType(flagValueType)) {
|
||||
executeChecked(hook, hookCode, hookMethod);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// before, error, and finally hooks shouldn't throw
|
||||
private <T> void executeChecked(Hook<T> hook, Consumer<Hook<T>> hookCode, String hookMethod) {
|
||||
try {
|
||||
hookCode.accept(hook);
|
||||
} catch (Exception exception) {
|
||||
log.error(
|
||||
"Unhandled exception when running {} hook {} (only 'after' hooks should throw)",
|
||||
hookMethod,
|
||||
hook.getClass(),
|
||||
exception);
|
||||
}
|
||||
}
|
||||
|
||||
// after hooks can throw in order to do validation
|
||||
private <T> void executeHooksUnchecked(FlagValueType flagValueType, List<Hook> hooks, Consumer<Hook<T>> hookCode) {
|
||||
if (hooks != null) {
|
||||
for (Hook hook : hooks) {
|
||||
if (hook.supportsFlagValueType(flagValueType)) {
|
||||
hookCode.accept(hook);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private EvaluationContext callBeforeHooks(
|
||||
FlagValueType flagValueType, HookContext hookCtx, List<Hook> hooks, Map<String, Object> hints) {
|
||||
// These traverse backwards from normal.
|
||||
List<Hook> reversedHooks = new ArrayList<>(hooks);
|
||||
Collections.reverse(reversedHooks);
|
||||
EvaluationContext context = hookCtx.getCtx();
|
||||
for (Hook hook : reversedHooks) {
|
||||
if (hook.supportsFlagValueType(flagValueType)) {
|
||||
Optional<EvaluationContext> optional =
|
||||
Optional.ofNullable(hook.before(hookCtx, hints)).orElse(Optional.empty());
|
||||
if (optional.isPresent()) {
|
||||
context = context.merge(optional.get());
|
||||
}
|
||||
}
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,106 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Delegate;
|
||||
|
||||
/**
|
||||
* The EvaluationContext is a container for arbitrary contextual data
|
||||
* that can be used as a basis for dynamic evaluation.
|
||||
* The ImmutableContext is an EvaluationContext implementation which is
|
||||
* threadsafe, and whose attributes can
|
||||
* not be modified after instantiation.
|
||||
*/
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
|
||||
public final class ImmutableContext implements EvaluationContext {
|
||||
|
||||
@Delegate(excludes = DelegateExclusions.class)
|
||||
private final ImmutableStructure structure;
|
||||
|
||||
/**
|
||||
* Create an immutable context with an empty targeting_key and attributes
|
||||
* provided.
|
||||
*/
|
||||
public ImmutableContext() {
|
||||
this(new HashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an immutable context with given targeting_key provided.
|
||||
*
|
||||
* @param targetingKey targeting key
|
||||
*/
|
||||
public ImmutableContext(String targetingKey) {
|
||||
this(targetingKey, new HashMap<>());
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an immutable context with an attributes provided.
|
||||
*
|
||||
* @param attributes evaluation context attributes
|
||||
*/
|
||||
public ImmutableContext(Map<String, Value> attributes) {
|
||||
this(null, attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an immutable context with given targetingKey and attributes provided.
|
||||
*
|
||||
* @param targetingKey targeting key
|
||||
* @param attributes evaluation context attributes
|
||||
*/
|
||||
public ImmutableContext(String targetingKey, Map<String, Value> attributes) {
|
||||
if (targetingKey != null && !targetingKey.trim().isEmpty()) {
|
||||
this.structure = new ImmutableStructure(targetingKey, attributes);
|
||||
} else {
|
||||
this.structure = new ImmutableStructure(attributes);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve targetingKey from the context.
|
||||
*/
|
||||
@Override
|
||||
public String getTargetingKey() {
|
||||
Value value = this.getValue(TARGETING_KEY);
|
||||
return value == null ? null : value.asString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges this EvaluationContext object with the passed EvaluationContext,
|
||||
* overriding in case of conflict.
|
||||
*
|
||||
* @param overridingContext overriding context
|
||||
* @return new, resulting merged context
|
||||
*/
|
||||
@Override
|
||||
public EvaluationContext merge(EvaluationContext overridingContext) {
|
||||
if (overridingContext == null || overridingContext.isEmpty()) {
|
||||
return new ImmutableContext(this.asUnmodifiableMap());
|
||||
}
|
||||
if (this.isEmpty()) {
|
||||
return new ImmutableContext(overridingContext.asUnmodifiableMap());
|
||||
}
|
||||
|
||||
Map<String, Value> attributes = this.asMap();
|
||||
EvaluationContext.mergeMaps(ImmutableStructure::new, attributes, overridingContext.asUnmodifiableMap());
|
||||
return new ImmutableContext(attributes);
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
private static class DelegateExclusions {
|
||||
@ExcludeFromGeneratedCoverageReport
|
||||
public <T extends Structure> Map<String, Value> merge(
|
||||
Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,198 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* Immutable Flag Metadata representation. Implementation is backed by a {@link Map} and immutability is provided
|
||||
* through builder and accessors.
|
||||
*/
|
||||
@Slf4j
|
||||
@EqualsAndHashCode
|
||||
public class ImmutableMetadata {
|
||||
private final Map<String, Object> metadata;
|
||||
|
||||
private ImmutableMetadata(Map<String, Object> metadata) {
|
||||
this.metadata = metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a {@link String} value for the given key. A {@code null} value is returned if the key does not exist
|
||||
* or if the value is of a different type.
|
||||
*
|
||||
* @param key flag metadata key to retrieve
|
||||
*/
|
||||
public String getString(final String key) {
|
||||
return getValue(key, String.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a {@link Integer} value for the given key. A {@code null} value is returned if the key does not exist
|
||||
* or if the value is of a different type.
|
||||
*
|
||||
* @param key flag metadata key to retrieve
|
||||
*/
|
||||
public Integer getInteger(final String key) {
|
||||
return getValue(key, Integer.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a {@link Long} value for the given key. A {@code null} value is returned if the key does not exist
|
||||
* or if the value is of a different type.
|
||||
*
|
||||
* @param key flag metadata key to retrieve
|
||||
*/
|
||||
public Long getLong(final String key) {
|
||||
return getValue(key, Long.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a {@link Float} value for the given key. A {@code null} value is returned if the key does not exist
|
||||
* or if the value is of a different type.
|
||||
*
|
||||
* @param key flag metadata key to retrieve
|
||||
*/
|
||||
public Float getFloat(final String key) {
|
||||
return getValue(key, Float.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a {@link Double} value for the given key. A {@code null} value is returned if the key does not exist
|
||||
* or if the value is of a different type.
|
||||
*
|
||||
* @param key flag metadata key to retrieve
|
||||
*/
|
||||
public Double getDouble(final String key) {
|
||||
return getValue(key, Double.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve a {@link Boolean} value for the given key. A {@code null} value is returned if the key does not exist
|
||||
* or if the value is of a different type.
|
||||
*
|
||||
* @param key flag metadata key to retrieve
|
||||
*/
|
||||
public Boolean getBoolean(final String key) {
|
||||
return getValue(key, Boolean.class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic value retrieval for the given key.
|
||||
*/
|
||||
public <T> T getValue(final String key, final Class<T> type) {
|
||||
final Object o = metadata.get(key);
|
||||
|
||||
if (o == null) {
|
||||
log.debug("Metadata key " + key + "does not exist");
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return type.cast(o);
|
||||
} catch (ClassCastException e) {
|
||||
log.debug("Error retrieving value for key " + key, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEmpty() {
|
||||
return metadata.isEmpty();
|
||||
}
|
||||
|
||||
public boolean isNotEmpty() {
|
||||
return !metadata.isEmpty();
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a builder for {@link ImmutableMetadata}.
|
||||
*/
|
||||
public static ImmutableMetadataBuilder builder() {
|
||||
return new ImmutableMetadataBuilder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Immutable builder for {@link ImmutableMetadata}.
|
||||
*/
|
||||
public static class ImmutableMetadataBuilder {
|
||||
private final Map<String, Object> metadata;
|
||||
|
||||
private ImmutableMetadataBuilder() {
|
||||
metadata = new HashMap<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add String value to the metadata.
|
||||
*
|
||||
* @param key flag metadata key to add
|
||||
* @param value flag metadata value to add
|
||||
*/
|
||||
public ImmutableMetadataBuilder addString(final String key, final String value) {
|
||||
metadata.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Integer value to the metadata.
|
||||
*
|
||||
* @param key flag metadata key to add
|
||||
* @param value flag metadata value to add
|
||||
*/
|
||||
public ImmutableMetadataBuilder addInteger(final String key, final Integer value) {
|
||||
metadata.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Long value to the metadata.
|
||||
*
|
||||
* @param key flag metadata key to add
|
||||
* @param value flag metadata value to add
|
||||
*/
|
||||
public ImmutableMetadataBuilder addLong(final String key, final Long value) {
|
||||
metadata.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Float value to the metadata.
|
||||
*
|
||||
* @param key flag metadata key to add
|
||||
* @param value flag metadata value to add
|
||||
*/
|
||||
public ImmutableMetadataBuilder addFloat(final String key, final Float value) {
|
||||
metadata.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Double value to the metadata.
|
||||
*
|
||||
* @param key flag metadata key to add
|
||||
* @param value flag metadata value to add
|
||||
*/
|
||||
public ImmutableMetadataBuilder addDouble(final String key, final Double value) {
|
||||
metadata.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Boolean value to the metadata.
|
||||
*
|
||||
* @param key flag metadata key to add
|
||||
* @param value flag metadata value to add
|
||||
*/
|
||||
public ImmutableMetadataBuilder addBoolean(final String key, final Boolean value) {
|
||||
metadata.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve {@link ImmutableMetadata} with provided key,value pairs.
|
||||
*/
|
||||
public ImmutableMetadata build() {
|
||||
return new ImmutableMetadata(this.metadata);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,87 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Map.Entry;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* {@link ImmutableStructure} represents a potentially nested object type which
|
||||
* is used to represent
|
||||
* structured data.
|
||||
* The ImmutableStructure is a Structure implementation which is threadsafe, and
|
||||
* whose attributes can
|
||||
* not be modified after instantiation. All references are clones.
|
||||
*/
|
||||
@ToString
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
|
||||
public final class ImmutableStructure extends AbstractStructure {
|
||||
|
||||
/**
|
||||
* create an immutable structure with the empty attributes.
|
||||
*/
|
||||
public ImmutableStructure() {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* create immutable structure with the given attributes.
|
||||
*
|
||||
* @param attributes attributes.
|
||||
*/
|
||||
public ImmutableStructure(Map<String, Value> attributes) {
|
||||
super(copyAttributes(attributes, null));
|
||||
}
|
||||
|
||||
ImmutableStructure(String targetingKey, Map<String, Value> attributes) {
|
||||
super(copyAttributes(attributes, targetingKey));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
return new HashSet<>(this.attributes.keySet());
|
||||
}
|
||||
|
||||
// getters
|
||||
@Override
|
||||
public Value getValue(String key) {
|
||||
Value value = attributes.get(key);
|
||||
return value != null ? value.clone() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all values.
|
||||
*
|
||||
* @return all attributes on the structure
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Value> asMap() {
|
||||
return copyAttributes(attributes);
|
||||
}
|
||||
|
||||
private static Map<String, Value> copyAttributes(Map<String, Value> in) {
|
||||
return copyAttributes(in, null);
|
||||
}
|
||||
|
||||
private static Map<String, Value> copyAttributes(Map<String, Value> in, String targetingKey) {
|
||||
Map<String, Value> copy = new HashMap<>();
|
||||
if (in != null) {
|
||||
for (Entry<String, Value> entry : in.entrySet()) {
|
||||
copy.put(
|
||||
entry.getKey(),
|
||||
Optional.ofNullable(entry.getValue())
|
||||
.map((Value val) -> val.clone())
|
||||
.orElse(null));
|
||||
}
|
||||
}
|
||||
if (targetingKey != null) {
|
||||
copy.put(EvaluationContext.TARGETING_KEY, new Value(targetingKey));
|
||||
}
|
||||
return copy;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,51 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import lombok.experimental.Delegate;
|
||||
|
||||
/**
|
||||
* ImmutableTrackingEventDetails represents data pertinent to a particular tracking event.
|
||||
*/
|
||||
public class ImmutableTrackingEventDetails implements TrackingEventDetails {
|
||||
|
||||
@Delegate(excludes = DelegateExclusions.class)
|
||||
private final ImmutableStructure structure;
|
||||
|
||||
private final Number value;
|
||||
|
||||
public ImmutableTrackingEventDetails() {
|
||||
this.value = null;
|
||||
this.structure = new ImmutableStructure();
|
||||
}
|
||||
|
||||
public ImmutableTrackingEventDetails(final Number value) {
|
||||
this.value = value;
|
||||
this.structure = new ImmutableStructure();
|
||||
}
|
||||
|
||||
public ImmutableTrackingEventDetails(final Number value, final Map<String, Value> attributes) {
|
||||
this.value = value;
|
||||
this.structure = new ImmutableStructure(attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the optional tracking value.
|
||||
*/
|
||||
public Optional<Number> getValue() {
|
||||
return Optional.ofNullable(value);
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
private static class DelegateExclusions {
|
||||
@ExcludeFromGeneratedCoverageReport
|
||||
public <T extends Structure> Map<String, Value> merge(
|
||||
Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* An extension point which can run around flag resolution. They are intended to be used as a way to add custom logic
|
||||
* to the lifecycle of flag evaluation.
|
||||
*
|
||||
* @see Hook
|
||||
*/
|
||||
public interface IntegerHook extends Hook<Integer> {
|
||||
|
||||
@Override
|
||||
default boolean supportsFlagValueType(FlagValueType flagValueType) {
|
||||
return FlagValueType.INTEGER == flagValueType;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package dev.openfeature.javasdk;
|
||||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* Holds identifying information about a given entity.
|
|
@ -0,0 +1,175 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Delegate;
|
||||
|
||||
/**
|
||||
* The EvaluationContext is a container for arbitrary contextual data
|
||||
* that can be used as a basis for dynamic evaluation.
|
||||
* The MutableContext is an EvaluationContext implementation which is not threadsafe, and whose attributes can
|
||||
* be modified after instantiation.
|
||||
*/
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
|
||||
public class MutableContext implements EvaluationContext {
|
||||
|
||||
@Delegate(excludes = DelegateExclusions.class)
|
||||
private final MutableStructure structure;
|
||||
|
||||
public MutableContext() {
|
||||
this(new HashMap<>());
|
||||
}
|
||||
|
||||
public MutableContext(String targetingKey) {
|
||||
this(targetingKey, new HashMap<>());
|
||||
}
|
||||
|
||||
public MutableContext(Map<String, Value> attributes) {
|
||||
this(null, new HashMap<>(attributes));
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mutable context with given targetingKey and attributes provided. TargetingKey should be non-null
|
||||
* and non-empty to be accepted.
|
||||
*
|
||||
* @param targetingKey targeting key
|
||||
* @param attributes evaluation context attributes
|
||||
*/
|
||||
public MutableContext(String targetingKey, Map<String, Value> attributes) {
|
||||
this.structure = new MutableStructure(new HashMap<>(attributes));
|
||||
if (targetingKey != null && !targetingKey.trim().isEmpty()) {
|
||||
this.structure.attributes.put(TARGETING_KEY, new Value(targetingKey));
|
||||
}
|
||||
}
|
||||
|
||||
// override @Delegate methods so that we can use "add" methods and still return MutableContext, not Structure
|
||||
public MutableContext add(String key, Boolean value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableContext add(String key, String value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableContext add(String key, Integer value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableContext add(String key, Double value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableContext add(String key, Instant value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableContext add(String key, Structure value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableContext add(String key, List<Value> value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Override or set targeting key for this mutable context. Value should be non-null and non-empty to be accepted.
|
||||
*/
|
||||
public MutableContext setTargetingKey(String targetingKey) {
|
||||
if (targetingKey != null && !targetingKey.trim().isEmpty()) {
|
||||
this.add(TARGETING_KEY, targetingKey);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve targetingKey from the context.
|
||||
*/
|
||||
@Override
|
||||
public String getTargetingKey() {
|
||||
Value value = this.getValue(TARGETING_KEY);
|
||||
return value == null ? null : value.asString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Merges this EvaluationContext objects with the second overriding the in case of conflict.
|
||||
*
|
||||
* @param overridingContext overriding context
|
||||
* @return resulting merged context
|
||||
*/
|
||||
@Override
|
||||
public EvaluationContext merge(EvaluationContext overridingContext) {
|
||||
if (overridingContext == null || overridingContext.isEmpty()) {
|
||||
return this;
|
||||
}
|
||||
if (this.isEmpty()) {
|
||||
return overridingContext;
|
||||
}
|
||||
|
||||
Map<String, Value> attributes = this.asMap();
|
||||
EvaluationContext.mergeMaps(MutableStructure::new, attributes, overridingContext.asUnmodifiableMap());
|
||||
return new MutableContext(attributes);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hidden class to tell Lombok not to copy these methods over via delegation.
|
||||
*/
|
||||
@SuppressWarnings("all")
|
||||
private static class DelegateExclusions {
|
||||
|
||||
@ExcludeFromGeneratedCoverageReport
|
||||
public <T extends Structure> Map<String, Value> merge(
|
||||
Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public MutableStructure add(String ignoredKey, Boolean ignoredValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public MutableStructure add(String ignoredKey, Double ignoredValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public MutableStructure add(String ignoredKey, String ignoredValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public MutableStructure add(String ignoredKey, Value ignoredValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public MutableStructure add(String ignoredKey, Integer ignoredValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public MutableStructure add(String ignoredKey, List<Value> ignoredValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public MutableStructure add(String ignoredKey, Structure ignoredValue) {
|
||||
return null;
|
||||
}
|
||||
|
||||
public MutableStructure add(String ignoredKey, Instant ignoredValue) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,91 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* {@link MutableStructure} represents a potentially nested object type which is used to represent
|
||||
* structured data.
|
||||
* The MutableStructure is a Structure implementation which is not threadsafe, and whose attributes can
|
||||
* be modified after instantiation.
|
||||
*/
|
||||
@ToString
|
||||
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType"})
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class MutableStructure extends AbstractStructure {
|
||||
|
||||
public MutableStructure() {
|
||||
super();
|
||||
}
|
||||
|
||||
public MutableStructure(Map<String, Value> attributes) {
|
||||
super(attributes);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> keySet() {
|
||||
return attributes.keySet();
|
||||
}
|
||||
|
||||
// getters
|
||||
@Override
|
||||
public Value getValue(String key) {
|
||||
return attributes.get(key);
|
||||
}
|
||||
|
||||
// adders
|
||||
public MutableStructure add(String key, Value value) {
|
||||
attributes.put(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableStructure add(String key, Boolean value) {
|
||||
attributes.put(key, new Value(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableStructure add(String key, String value) {
|
||||
attributes.put(key, new Value(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableStructure add(String key, Integer value) {
|
||||
attributes.put(key, new Value(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableStructure add(String key, Double value) {
|
||||
attributes.put(key, new Value(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableStructure add(String key, Instant value) {
|
||||
attributes.put(key, new Value(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableStructure add(String key, Structure value) {
|
||||
attributes.put(key, new Value(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableStructure add(String key, List<Value> value) {
|
||||
attributes.put(key, new Value(value));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all values.
|
||||
*
|
||||
* @return all attributes on the structure
|
||||
*/
|
||||
@Override
|
||||
public Map<String, Value> asMap() {
|
||||
return new HashMap<>(attributes);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.internal.ExcludeFromGeneratedCoverageReport;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.function.Function;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.ToString;
|
||||
import lombok.experimental.Delegate;
|
||||
|
||||
/**
|
||||
* MutableTrackingEventDetails represents data pertinent to a particular tracking event.
|
||||
*/
|
||||
@EqualsAndHashCode
|
||||
@ToString
|
||||
public class MutableTrackingEventDetails implements TrackingEventDetails {
|
||||
|
||||
private final Number value;
|
||||
|
||||
@Delegate(excludes = MutableTrackingEventDetails.DelegateExclusions.class)
|
||||
private final MutableStructure structure;
|
||||
|
||||
public MutableTrackingEventDetails() {
|
||||
this.value = null;
|
||||
this.structure = new MutableStructure();
|
||||
}
|
||||
|
||||
public MutableTrackingEventDetails(final Number value) {
|
||||
this.value = value;
|
||||
this.structure = new MutableStructure();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the optional tracking value.
|
||||
*/
|
||||
public Optional<Number> getValue() {
|
||||
return Optional.ofNullable(value);
|
||||
}
|
||||
|
||||
// override @Delegate methods so that we can use "add" methods and still return MutableTrackingEventDetails,
|
||||
// not Structure
|
||||
public MutableTrackingEventDetails add(String key, Boolean value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableTrackingEventDetails add(String key, String value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableTrackingEventDetails add(String key, Integer value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableTrackingEventDetails add(String key, Double value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableTrackingEventDetails add(String key, Instant value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableTrackingEventDetails add(String key, Structure value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableTrackingEventDetails add(String key, List<Value> value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MutableTrackingEventDetails add(String key, Value value) {
|
||||
this.structure.add(key, value);
|
||||
return this;
|
||||
}
|
||||
|
||||
@SuppressWarnings("all")
|
||||
private static class DelegateExclusions {
|
||||
@ExcludeFromGeneratedCoverageReport
|
||||
public <T extends Structure> Map<String, Value> merge(
|
||||
Function<Map<String, Value>, Structure> newStructure,
|
||||
Map<String, Value> base,
|
||||
Map<String, Value> overriding) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
package dev.openfeature.javasdk;
|
||||
package dev.openfeature.sdk;
|
||||
|
||||
import lombok.Getter;
|
||||
|
||||
|
@ -7,67 +7,64 @@ import lombok.Getter;
|
|||
*/
|
||||
public class NoOpProvider implements FeatureProvider {
|
||||
public static final String PASSED_IN_DEFAULT = "Passed in default";
|
||||
|
||||
@Getter
|
||||
private final String name = "No-op Provider";
|
||||
|
||||
// The Noop provider is ALWAYS NOT_READY, otherwise READY handlers would run immediately when attached.
|
||||
@Override
|
||||
public Metadata getMetadata() {
|
||||
return new Metadata() {
|
||||
@Override
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
};
|
||||
public ProviderState getState() {
|
||||
return ProviderState.NOT_READY;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public Metadata getMetadata() {
|
||||
return () -> name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Boolean> getBooleanEvaluation(String key, Boolean defaultValue, EvaluationContext ctx) {
|
||||
return ProviderEvaluation.<Boolean>builder()
|
||||
.value(defaultValue)
|
||||
.variant(PASSED_IN_DEFAULT)
|
||||
.reason(Reason.DEFAULT)
|
||||
.reason(Reason.DEFAULT.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public ProviderEvaluation<String> getStringEvaluation(String key, String defaultValue, EvaluationContext ctx) {
|
||||
return ProviderEvaluation.<String>builder()
|
||||
.value(defaultValue)
|
||||
.variant(PASSED_IN_DEFAULT)
|
||||
.reason(Reason.DEFAULT)
|
||||
.reason(Reason.DEFAULT.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public ProviderEvaluation<Integer> getIntegerEvaluation(String key, Integer defaultValue, EvaluationContext ctx) {
|
||||
return ProviderEvaluation.<Integer>builder()
|
||||
.value(defaultValue)
|
||||
.variant(PASSED_IN_DEFAULT)
|
||||
.reason(Reason.DEFAULT)
|
||||
.reason(Reason.DEFAULT.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx,
|
||||
FlagEvaluationOptions options) {
|
||||
public ProviderEvaluation<Double> getDoubleEvaluation(String key, Double defaultValue, EvaluationContext ctx) {
|
||||
return ProviderEvaluation.<Double>builder()
|
||||
.value(defaultValue)
|
||||
.variant(PASSED_IN_DEFAULT)
|
||||
.reason(Reason.DEFAULT)
|
||||
.reason(Reason.DEFAULT.toString())
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ProviderEvaluation<T> getObjectEvaluation(String key, T defaultValue,
|
||||
EvaluationContext invocationContext,
|
||||
FlagEvaluationOptions options) {
|
||||
return ProviderEvaluation.<T>builder()
|
||||
public ProviderEvaluation<Value> getObjectEvaluation(
|
||||
String key, Value defaultValue, EvaluationContext invocationContext) {
|
||||
return ProviderEvaluation.<Value>builder()
|
||||
.value(defaultValue)
|
||||
.variant(PASSED_IN_DEFAULT)
|
||||
.reason(Reason.DEFAULT)
|
||||
.reason(Reason.DEFAULT.toString())
|
||||
.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* A {@link TransactionContextPropagator} that simply returns empty context.
|
||||
*/
|
||||
public class NoOpTransactionContextPropagator implements TransactionContextPropagator {
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @return empty immutable context
|
||||
*/
|
||||
@Override
|
||||
public EvaluationContext getTransactionContext() {
|
||||
return new ImmutableContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setTransactionContext(EvaluationContext evaluationContext) {}
|
||||
}
|
|
@ -0,0 +1,461 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.OpenFeatureError;
|
||||
import dev.openfeature.sdk.internal.AutoCloseableLock;
|
||||
import dev.openfeature.sdk.internal.AutoCloseableReentrantReadWriteLock;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* A global singleton which holds base configuration for the OpenFeature
|
||||
* library.
|
||||
* Configuration here will be shared across all {@link Client}s.
|
||||
*/
|
||||
@Slf4j
|
||||
@SuppressWarnings("PMD.UnusedLocalVariable")
|
||||
public class OpenFeatureAPI implements EventBus<OpenFeatureAPI> {
|
||||
// package-private multi-read/single-write lock
|
||||
static AutoCloseableReentrantReadWriteLock lock = new AutoCloseableReentrantReadWriteLock();
|
||||
private final ConcurrentLinkedQueue<Hook> apiHooks;
|
||||
private ProviderRepository providerRepository;
|
||||
private EventSupport eventSupport;
|
||||
private final AtomicReference<EvaluationContext> evaluationContext = new AtomicReference<>();
|
||||
private TransactionContextPropagator transactionContextPropagator;
|
||||
|
||||
protected OpenFeatureAPI() {
|
||||
apiHooks = new ConcurrentLinkedQueue<>();
|
||||
providerRepository = new ProviderRepository(this);
|
||||
eventSupport = new EventSupport();
|
||||
transactionContextPropagator = new NoOpTransactionContextPropagator();
|
||||
}
|
||||
|
||||
private static class SingletonHolder {
|
||||
private static final OpenFeatureAPI INSTANCE = new OpenFeatureAPI();
|
||||
}
|
||||
|
||||
/**
|
||||
* Provisions the {@link OpenFeatureAPI} singleton (if needed) and returns it.
|
||||
*
|
||||
* @return The singleton instance.
|
||||
*/
|
||||
public static OpenFeatureAPI getInstance() {
|
||||
return SingletonHolder.INSTANCE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata about the default provider.
|
||||
*
|
||||
* @return the provider metadata
|
||||
*/
|
||||
public Metadata getProviderMetadata() {
|
||||
return getProvider().getMetadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get metadata about a registered provider using the client name.
|
||||
* An unbound or empty client name will return metadata from the default provider.
|
||||
*
|
||||
* @param domain an identifier which logically binds clients with providers
|
||||
* @return the provider metadata
|
||||
*/
|
||||
public Metadata getProviderMetadata(String domain) {
|
||||
return getProvider(domain).getMetadata();
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory function for creating new, OpenFeature client.
|
||||
* Clients can contain their own state (e.g. logger, hook, context).
|
||||
* Multiple clients can be used to segment feature flag configuration.
|
||||
* All un-named or unbound clients use the default provider.
|
||||
*
|
||||
* @return a new client instance
|
||||
*/
|
||||
public Client getClient() {
|
||||
return getClient(null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory function for creating new domainless OpenFeature client.
|
||||
* Clients can contain their own state (e.g. logger, hook, context).
|
||||
* Multiple clients can be used to segment feature flag configuration.
|
||||
* If there is already a provider bound to this domain, this provider will be used.
|
||||
* Otherwise, the default provider is used until a provider is assigned to that domain.
|
||||
*
|
||||
* @param domain an identifier which logically binds clients with providers
|
||||
* @return a new client instance
|
||||
*/
|
||||
public Client getClient(String domain) {
|
||||
return getClient(domain, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* A factory function for creating new domainless OpenFeature client.
|
||||
* Clients can contain their own state (e.g. logger, hook, context).
|
||||
* Multiple clients can be used to segment feature flag configuration.
|
||||
* If there is already a provider bound to this domain, this provider will be used.
|
||||
* Otherwise, the default provider is used until a provider is assigned to that domain.
|
||||
*
|
||||
* @param domain a identifier which logically binds clients with providers
|
||||
* @param version a version identifier
|
||||
* @return a new client instance
|
||||
*/
|
||||
public Client getClient(String domain, String version) {
|
||||
return new OpenFeatureClient(this, domain, version);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the global evaluation context, which will be used for all evaluations.
|
||||
*
|
||||
* @param evaluationContext the context
|
||||
* @return api instance
|
||||
*/
|
||||
public OpenFeatureAPI setEvaluationContext(EvaluationContext evaluationContext) {
|
||||
this.evaluationContext.set(evaluationContext);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the global evaluation context, which will be used for all evaluations.
|
||||
*
|
||||
* @return evaluation context
|
||||
*/
|
||||
public EvaluationContext getEvaluationContext() {
|
||||
return evaluationContext.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the transaction context propagator.
|
||||
*/
|
||||
public TransactionContextPropagator getTransactionContextPropagator() {
|
||||
try (AutoCloseableLock ignored = lock.readLockAutoCloseable()) {
|
||||
return this.transactionContextPropagator;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the transaction context propagator.
|
||||
*
|
||||
* @throws IllegalArgumentException if {@code transactionContextPropagator} is null
|
||||
*/
|
||||
public void setTransactionContextPropagator(TransactionContextPropagator transactionContextPropagator) {
|
||||
if (transactionContextPropagator == null) {
|
||||
throw new IllegalArgumentException("Transaction context propagator cannot be null");
|
||||
}
|
||||
try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) {
|
||||
this.transactionContextPropagator = transactionContextPropagator;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the currently defined transaction context using the registered transaction
|
||||
* context propagator.
|
||||
*
|
||||
* @return {@link EvaluationContext} The current transaction context
|
||||
*/
|
||||
EvaluationContext getTransactionContext() {
|
||||
return this.transactionContextPropagator.getTransactionContext();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the transaction context using the registered transaction context propagator.
|
||||
*/
|
||||
public void setTransactionContext(EvaluationContext evaluationContext) {
|
||||
this.transactionContextPropagator.setTransactionContext(evaluationContext);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default provider.
|
||||
*/
|
||||
public void setProvider(FeatureProvider provider) {
|
||||
try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) {
|
||||
providerRepository.setProvider(
|
||||
provider,
|
||||
this::attachEventProvider,
|
||||
this::emitReady,
|
||||
this::detachEventProvider,
|
||||
this::emitError,
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a provider for a domain.
|
||||
*
|
||||
* @param domain The domain to bind the provider to.
|
||||
* @param provider The provider to set.
|
||||
*/
|
||||
public void setProvider(String domain, FeatureProvider provider) {
|
||||
try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) {
|
||||
providerRepository.setProvider(
|
||||
domain,
|
||||
provider,
|
||||
this::attachEventProvider,
|
||||
this::emitReady,
|
||||
this::detachEventProvider,
|
||||
this::emitError,
|
||||
false);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the default provider and waits for its initialization to complete.
|
||||
*
|
||||
* <p>Note: If the provider fails during initialization, an {@link OpenFeatureError} will be thrown.
|
||||
* It is recommended to wrap this call in a try-catch block to handle potential initialization failures gracefully.
|
||||
*
|
||||
* @param provider the {@link FeatureProvider} to set as the default.
|
||||
* @throws OpenFeatureError if the provider fails during initialization.
|
||||
*/
|
||||
public void setProviderAndWait(FeatureProvider provider) throws OpenFeatureError {
|
||||
try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) {
|
||||
providerRepository.setProvider(
|
||||
provider,
|
||||
this::attachEventProvider,
|
||||
this::emitReady,
|
||||
this::detachEventProvider,
|
||||
this::emitErrorAndThrow,
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a provider for a domain and wait for initialization to finish.
|
||||
*
|
||||
* <p>Note: If the provider fails during initialization, an {@link OpenFeatureError} will be thrown.
|
||||
* It is recommended to wrap this call in a try-catch block to handle potential initialization failures gracefully.
|
||||
*
|
||||
* @param domain The domain to bind the provider to.
|
||||
* @param provider The provider to set.
|
||||
* @throws OpenFeatureError if the provider fails during initialization.
|
||||
*/
|
||||
public void setProviderAndWait(String domain, FeatureProvider provider) throws OpenFeatureError {
|
||||
try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) {
|
||||
providerRepository.setProvider(
|
||||
domain,
|
||||
provider,
|
||||
this::attachEventProvider,
|
||||
this::emitReady,
|
||||
this::detachEventProvider,
|
||||
this::emitErrorAndThrow,
|
||||
true);
|
||||
}
|
||||
}
|
||||
|
||||
private void attachEventProvider(FeatureProvider provider) {
|
||||
if (provider instanceof EventProvider) {
|
||||
((EventProvider) provider).attach(this::runHandlersForProvider);
|
||||
}
|
||||
}
|
||||
|
||||
private void emitReady(FeatureProvider provider) {
|
||||
runHandlersForProvider(
|
||||
provider,
|
||||
ProviderEvent.PROVIDER_READY,
|
||||
ProviderEventDetails.builder().build());
|
||||
}
|
||||
|
||||
private void detachEventProvider(FeatureProvider provider) {
|
||||
if (provider instanceof EventProvider) {
|
||||
((EventProvider) provider).detach();
|
||||
}
|
||||
}
|
||||
|
||||
private void emitError(FeatureProvider provider, OpenFeatureError exception) {
|
||||
runHandlersForProvider(
|
||||
provider,
|
||||
ProviderEvent.PROVIDER_ERROR,
|
||||
ProviderEventDetails.builder().message(exception.getMessage()).build());
|
||||
}
|
||||
|
||||
private void emitErrorAndThrow(FeatureProvider provider, OpenFeatureError exception) throws OpenFeatureError {
|
||||
this.emitError(provider, exception);
|
||||
throw exception;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default provider.
|
||||
*/
|
||||
public FeatureProvider getProvider() {
|
||||
return providerRepository.getProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a provider for a domain. If not found, return the default.
|
||||
*
|
||||
* @param domain The domain to look for.
|
||||
* @return A named {@link FeatureProvider}
|
||||
*/
|
||||
public FeatureProvider getProvider(String domain) {
|
||||
return providerRepository.getProvider(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds hooks for globally, used for all evaluations.
|
||||
* Hooks are run in the order they're added in the before stage. They are run in reverse order for all other stages.
|
||||
*
|
||||
* @param hooks The hook to add.
|
||||
*/
|
||||
public void addHooks(Hook... hooks) {
|
||||
this.apiHooks.addAll(Arrays.asList(hooks));
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the hooks associated to this client.
|
||||
*
|
||||
* @return A list of {@link Hook}s.
|
||||
*/
|
||||
public List<Hook> getHooks() {
|
||||
return new ArrayList<>(this.apiHooks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a reference to the collection of {@link Hook}s.
|
||||
*
|
||||
* @return The collection of {@link Hook}s.
|
||||
*/
|
||||
Collection<Hook> getMutableHooks() {
|
||||
return this.apiHooks;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all hooks.
|
||||
*/
|
||||
public void clearHooks() {
|
||||
this.apiHooks.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Shut down and reset the current status of OpenFeature API.
|
||||
* This call cleans up all active providers and attempts to shut down internal
|
||||
* event handling mechanisms.
|
||||
* Once shut down is complete, API is reset and ready to use again.
|
||||
*/
|
||||
public void shutdown() {
|
||||
try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) {
|
||||
providerRepository.shutdown();
|
||||
eventSupport.shutdown();
|
||||
|
||||
providerRepository = new ProviderRepository(this);
|
||||
eventSupport = new EventSupport();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OpenFeatureAPI onProviderReady(Consumer<EventDetails> handler) {
|
||||
return this.on(ProviderEvent.PROVIDER_READY, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OpenFeatureAPI onProviderConfigurationChanged(Consumer<EventDetails> handler) {
|
||||
return this.on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OpenFeatureAPI onProviderStale(Consumer<EventDetails> handler) {
|
||||
return this.on(ProviderEvent.PROVIDER_STALE, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OpenFeatureAPI onProviderError(Consumer<EventDetails> handler) {
|
||||
return this.on(ProviderEvent.PROVIDER_ERROR, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OpenFeatureAPI on(ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) {
|
||||
this.eventSupport.addGlobalHandler(event, handler);
|
||||
return this;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OpenFeatureAPI removeHandler(ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) {
|
||||
this.eventSupport.removeGlobalHandler(event, handler);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
void removeHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) {
|
||||
eventSupport.removeClientHandler(domain, event, handler);
|
||||
}
|
||||
}
|
||||
|
||||
void addHandler(String domain, ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
try (AutoCloseableLock ignored = lock.writeLockAutoCloseable()) {
|
||||
// if the provider is in the state associated with event, run immediately
|
||||
if (Optional.ofNullable(this.providerRepository.getProviderState(domain))
|
||||
.orElse(ProviderState.READY)
|
||||
.matchesEvent(event)) {
|
||||
eventSupport.runHandler(
|
||||
handler, EventDetails.builder().domain(domain).build());
|
||||
}
|
||||
eventSupport.addClientHandler(domain, event, handler);
|
||||
}
|
||||
}
|
||||
|
||||
FeatureProviderStateManager getFeatureProviderStateManager(String domain) {
|
||||
return providerRepository.getFeatureProviderStateManager(domain);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs the handlers associated with a particular provider.
|
||||
*
|
||||
* @param provider the provider from where this event originated
|
||||
* @param event the event type
|
||||
* @param details the event details
|
||||
*/
|
||||
private void runHandlersForProvider(FeatureProvider provider, ProviderEvent event, ProviderEventDetails details) {
|
||||
try (AutoCloseableLock ignored = lock.readLockAutoCloseable()) {
|
||||
|
||||
List<String> domainsForProvider = providerRepository.getDomainsForProvider(provider);
|
||||
|
||||
final String providerName = Optional.ofNullable(provider.getMetadata())
|
||||
.map(Metadata::getName)
|
||||
.orElse(null);
|
||||
|
||||
// run the global handlers
|
||||
eventSupport.runGlobalHandlers(event, EventDetails.fromProviderEventDetails(details, providerName));
|
||||
|
||||
// run the handlers associated with domains for this provider
|
||||
domainsForProvider.forEach(domain -> eventSupport.runClientHandlers(
|
||||
domain, event, EventDetails.fromProviderEventDetails(details, providerName, domain)));
|
||||
|
||||
if (providerRepository.isDefaultProvider(provider)) {
|
||||
// run handlers for clients that have no bound providers (since this is the default)
|
||||
Set<String> allDomainNames = eventSupport.getAllDomainNames();
|
||||
Set<String> boundDomains = providerRepository.getAllBoundDomains();
|
||||
allDomainNames.removeAll(boundDomains);
|
||||
allDomainNames.forEach(domain -> eventSupport.runClientHandlers(
|
||||
domain, event, EventDetails.fromProviderEventDetails(details, providerName, domain)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,518 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.ExceptionUtils;
|
||||
import dev.openfeature.sdk.exceptions.FatalError;
|
||||
import dev.openfeature.sdk.exceptions.GeneralError;
|
||||
import dev.openfeature.sdk.exceptions.OpenFeatureError;
|
||||
import dev.openfeature.sdk.exceptions.ProviderNotReadyError;
|
||||
import dev.openfeature.sdk.internal.ObjectUtils;
|
||||
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import lombok.Getter;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
/**
|
||||
* OpenFeature Client implementation.
|
||||
* You should not instantiate this or reference this class.
|
||||
* Use the dev.openfeature.sdk.Client interface instead.
|
||||
*
|
||||
* @see Client
|
||||
* @deprecated // TODO: eventually we will make this non-public. See issue #872
|
||||
*/
|
||||
@Slf4j
|
||||
@SuppressWarnings({
|
||||
"PMD.DataflowAnomalyAnalysis",
|
||||
"PMD.BeanMembersShouldSerialize",
|
||||
"PMD.UnusedLocalVariable",
|
||||
"unchecked",
|
||||
"rawtypes"
|
||||
})
|
||||
@Deprecated() // TODO: eventually we will make this non-public. See issue #872
|
||||
public class OpenFeatureClient implements Client {
|
||||
|
||||
private final OpenFeatureAPI openfeatureApi;
|
||||
|
||||
@Getter
|
||||
private final String domain;
|
||||
|
||||
@Getter
|
||||
private final String version;
|
||||
|
||||
private final ConcurrentLinkedQueue<Hook> clientHooks;
|
||||
private final HookSupport hookSupport;
|
||||
private final AtomicReference<EvaluationContext> evaluationContext = new AtomicReference<>();
|
||||
|
||||
/**
|
||||
* Deprecated public constructor. Use OpenFeature.API.getClient() instead.
|
||||
*
|
||||
* @param openFeatureAPI Backing global singleton
|
||||
* @param domain An identifier which logically binds clients with
|
||||
* providers (used by observability tools).
|
||||
* @param version Version of the client (used by observability tools).
|
||||
* @deprecated Do not use this constructor. It's for internal use only.
|
||||
* Clients created using it will not run event handlers.
|
||||
* Use the OpenFeatureAPI's getClient factory method instead.
|
||||
*/
|
||||
@Deprecated() // TODO: eventually we will make this non-public. See issue #872
|
||||
public OpenFeatureClient(OpenFeatureAPI openFeatureAPI, String domain, String version) {
|
||||
this.openfeatureApi = openFeatureAPI;
|
||||
this.domain = domain;
|
||||
this.version = version;
|
||||
this.clientHooks = new ConcurrentLinkedQueue<>();
|
||||
this.hookSupport = new HookSupport();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public ProviderState getProviderState() {
|
||||
return openfeatureApi.getFeatureProviderStateManager(domain).getState();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void track(String trackingEventName) {
|
||||
validateTrackingEventName(trackingEventName);
|
||||
invokeTrack(trackingEventName, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void track(String trackingEventName, EvaluationContext context) {
|
||||
validateTrackingEventName(trackingEventName);
|
||||
Objects.requireNonNull(context);
|
||||
invokeTrack(trackingEventName, context, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void track(String trackingEventName, TrackingEventDetails details) {
|
||||
validateTrackingEventName(trackingEventName);
|
||||
Objects.requireNonNull(details);
|
||||
invokeTrack(trackingEventName, null, details);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void track(String trackingEventName, EvaluationContext context, TrackingEventDetails details) {
|
||||
validateTrackingEventName(trackingEventName);
|
||||
Objects.requireNonNull(context);
|
||||
Objects.requireNonNull(details);
|
||||
invokeTrack(trackingEventName, mergeEvaluationContext(context), details);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OpenFeatureClient addHooks(Hook... hooks) {
|
||||
this.clientHooks.addAll(Arrays.asList(hooks));
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public List<Hook> getHooks() {
|
||||
return new ArrayList<>(this.clientHooks);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public OpenFeatureClient setEvaluationContext(EvaluationContext evaluationContext) {
|
||||
this.evaluationContext.set(evaluationContext);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public EvaluationContext getEvaluationContext() {
|
||||
return this.evaluationContext.get();
|
||||
}
|
||||
|
||||
@SuppressFBWarnings(
|
||||
value = {"REC_CATCH_EXCEPTION"},
|
||||
justification = "We don't want to allow any exception to reach the user. "
|
||||
+ "Instead, we return an evaluation result with the appropriate error code.")
|
||||
private <T> FlagEvaluationDetails<T> evaluateFlag(
|
||||
FlagValueType type, String key, T defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
var flagOptions = ObjectUtils.defaultIfNull(
|
||||
options, () -> FlagEvaluationOptions.builder().build());
|
||||
var hints = Collections.unmodifiableMap(flagOptions.getHookHints());
|
||||
|
||||
FlagEvaluationDetails<T> details = null;
|
||||
List<Hook> mergedHooks = null;
|
||||
HookContext<T> afterHookContext = null;
|
||||
|
||||
try {
|
||||
var stateManager = openfeatureApi.getFeatureProviderStateManager(this.domain);
|
||||
// provider must be accessed once to maintain a consistent reference
|
||||
var provider = stateManager.getProvider();
|
||||
var state = stateManager.getState();
|
||||
|
||||
mergedHooks = ObjectUtils.merge(
|
||||
provider.getProviderHooks(), flagOptions.getHooks(), clientHooks, openfeatureApi.getMutableHooks());
|
||||
|
||||
var mergedCtx = hookSupport.beforeHooks(
|
||||
type,
|
||||
HookContext.from(
|
||||
key,
|
||||
type,
|
||||
this.getMetadata(),
|
||||
provider.getMetadata(),
|
||||
mergeEvaluationContext(ctx),
|
||||
defaultValue),
|
||||
mergedHooks,
|
||||
hints);
|
||||
|
||||
afterHookContext =
|
||||
HookContext.from(key, type, this.getMetadata(), provider.getMetadata(), mergedCtx, defaultValue);
|
||||
|
||||
// "short circuit" if the provider is in NOT_READY or FATAL state
|
||||
if (ProviderState.NOT_READY.equals(state)) {
|
||||
throw new ProviderNotReadyError("Provider not yet initialized");
|
||||
}
|
||||
if (ProviderState.FATAL.equals(state)) {
|
||||
throw new FatalError("Provider is in an irrecoverable error state");
|
||||
}
|
||||
|
||||
var providerEval =
|
||||
(ProviderEvaluation<T>) createProviderEvaluation(type, key, defaultValue, provider, mergedCtx);
|
||||
|
||||
details = FlagEvaluationDetails.from(providerEval, key);
|
||||
if (details.getErrorCode() != null) {
|
||||
var error =
|
||||
ExceptionUtils.instantiateErrorByErrorCode(details.getErrorCode(), details.getErrorMessage());
|
||||
enrichDetailsWithErrorDefaults(defaultValue, details);
|
||||
hookSupport.errorHooks(type, afterHookContext, error, mergedHooks, hints);
|
||||
} else {
|
||||
hookSupport.afterHooks(type, afterHookContext, details, mergedHooks, hints);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
if (details == null) {
|
||||
details = FlagEvaluationDetails.<T>builder().flagKey(key).build();
|
||||
}
|
||||
if (e instanceof OpenFeatureError) {
|
||||
details.setErrorCode(((OpenFeatureError) e).getErrorCode());
|
||||
} else {
|
||||
details.setErrorCode(ErrorCode.GENERAL);
|
||||
}
|
||||
details.setErrorMessage(e.getMessage());
|
||||
enrichDetailsWithErrorDefaults(defaultValue, details);
|
||||
hookSupport.errorHooks(type, afterHookContext, e, mergedHooks, hints);
|
||||
} finally {
|
||||
hookSupport.afterAllHooks(type, afterHookContext, details, mergedHooks, hints);
|
||||
}
|
||||
|
||||
return details;
|
||||
}
|
||||
|
||||
private static <T> void enrichDetailsWithErrorDefaults(T defaultValue, FlagEvaluationDetails<T> details) {
|
||||
details.setValue(defaultValue);
|
||||
details.setReason(Reason.ERROR.toString());
|
||||
}
|
||||
|
||||
private static void validateTrackingEventName(String str) {
|
||||
Objects.requireNonNull(str);
|
||||
if (str.isEmpty()) {
|
||||
throw new IllegalArgumentException("trackingEventName cannot be empty");
|
||||
}
|
||||
}
|
||||
|
||||
private void invokeTrack(String trackingEventName, EvaluationContext context, TrackingEventDetails details) {
|
||||
openfeatureApi
|
||||
.getFeatureProviderStateManager(domain)
|
||||
.getProvider()
|
||||
.track(trackingEventName, mergeEvaluationContext(context), details);
|
||||
}
|
||||
|
||||
/**
|
||||
* Merge invocation contexts with API, transaction and client contexts.
|
||||
* Does not merge before context.
|
||||
*
|
||||
* @param invocationContext invocation context
|
||||
* @return merged evaluation context
|
||||
*/
|
||||
private EvaluationContext mergeEvaluationContext(EvaluationContext invocationContext) {
|
||||
final EvaluationContext apiContext = openfeatureApi.getEvaluationContext();
|
||||
final EvaluationContext clientContext = evaluationContext.get();
|
||||
final EvaluationContext transactionContext = openfeatureApi.getTransactionContext();
|
||||
return mergeContextMaps(apiContext, transactionContext, clientContext, invocationContext);
|
||||
}
|
||||
|
||||
private EvaluationContext mergeContextMaps(EvaluationContext... contexts) {
|
||||
// avoid any unnecessary context instantiations and stream usage here; this is
|
||||
// called with every evaluation.
|
||||
Map merged = new HashMap<>();
|
||||
for (EvaluationContext evaluationContext : contexts) {
|
||||
if (evaluationContext != null && !evaluationContext.isEmpty()) {
|
||||
EvaluationContext.mergeMaps(ImmutableStructure::new, merged, evaluationContext.asUnmodifiableMap());
|
||||
}
|
||||
}
|
||||
return new ImmutableContext(merged);
|
||||
}
|
||||
|
||||
private <T> ProviderEvaluation<?> createProviderEvaluation(
|
||||
FlagValueType type,
|
||||
String key,
|
||||
T defaultValue,
|
||||
FeatureProvider provider,
|
||||
EvaluationContext invocationContext) {
|
||||
switch (type) {
|
||||
case BOOLEAN:
|
||||
return provider.getBooleanEvaluation(key, (Boolean) defaultValue, invocationContext);
|
||||
case STRING:
|
||||
return provider.getStringEvaluation(key, (String) defaultValue, invocationContext);
|
||||
case INTEGER:
|
||||
return provider.getIntegerEvaluation(key, (Integer) defaultValue, invocationContext);
|
||||
case DOUBLE:
|
||||
return provider.getDoubleEvaluation(key, (Double) defaultValue, invocationContext);
|
||||
case OBJECT:
|
||||
return provider.getObjectEvaluation(key, (Value) defaultValue, invocationContext);
|
||||
default:
|
||||
throw new GeneralError("Unknown flag type");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getBooleanValue(String key, Boolean defaultValue) {
|
||||
return getBooleanDetails(key, defaultValue).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getBooleanValue(String key, Boolean defaultValue, EvaluationContext ctx) {
|
||||
return getBooleanDetails(key, defaultValue, ctx).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Boolean getBooleanValue(
|
||||
String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return getBooleanDetails(key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue) {
|
||||
return getBooleanDetails(key, defaultValue, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Boolean> getBooleanDetails(String key, Boolean defaultValue, EvaluationContext ctx) {
|
||||
return getBooleanDetails(
|
||||
key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Boolean> getBooleanDetails(
|
||||
String key, Boolean defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.BOOLEAN, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStringValue(String key, String defaultValue) {
|
||||
return getStringDetails(key, defaultValue).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStringValue(String key, String defaultValue, EvaluationContext ctx) {
|
||||
return getStringDetails(key, defaultValue, ctx).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getStringValue(
|
||||
String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return getStringDetails(key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue) {
|
||||
return getStringDetails(key, defaultValue, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<String> getStringDetails(String key, String defaultValue, EvaluationContext ctx) {
|
||||
return getStringDetails(
|
||||
key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<String> getStringDetails(
|
||||
String key, String defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.STRING, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getIntegerValue(String key, Integer defaultValue) {
|
||||
return getIntegerDetails(key, defaultValue).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getIntegerValue(String key, Integer defaultValue, EvaluationContext ctx) {
|
||||
return getIntegerDetails(key, defaultValue, ctx).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getIntegerValue(
|
||||
String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return getIntegerDetails(key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue) {
|
||||
return getIntegerDetails(key, defaultValue, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Integer> getIntegerDetails(String key, Integer defaultValue, EvaluationContext ctx) {
|
||||
return getIntegerDetails(
|
||||
key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Integer> getIntegerDetails(
|
||||
String key, Integer defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.INTEGER, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getDoubleValue(String key, Double defaultValue) {
|
||||
return getDoubleValue(key, defaultValue, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getDoubleValue(String key, Double defaultValue, EvaluationContext ctx) {
|
||||
return getDoubleValue(key, defaultValue, ctx, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Double getDoubleValue(
|
||||
String key, Double defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options)
|
||||
.getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue) {
|
||||
return getDoubleDetails(key, defaultValue, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Double> getDoubleDetails(String key, Double defaultValue, EvaluationContext ctx) {
|
||||
return getDoubleDetails(key, defaultValue, ctx, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Double> getDoubleDetails(
|
||||
String key, Double defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.DOUBLE, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Value getObjectValue(String key, Value defaultValue) {
|
||||
return getObjectDetails(key, defaultValue).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx) {
|
||||
return getObjectDetails(key, defaultValue, ctx).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Value getObjectValue(String key, Value defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return getObjectDetails(key, defaultValue, ctx, options).getValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue) {
|
||||
return getObjectDetails(key, defaultValue, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Value> getObjectDetails(String key, Value defaultValue, EvaluationContext ctx) {
|
||||
return getObjectDetails(
|
||||
key, defaultValue, ctx, FlagEvaluationOptions.builder().build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public FlagEvaluationDetails<Value> getObjectDetails(
|
||||
String key, Value defaultValue, EvaluationContext ctx, FlagEvaluationOptions options) {
|
||||
return this.evaluateFlag(FlagValueType.OBJECT, key, defaultValue, ctx, options);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ClientMetadata getMetadata() {
|
||||
return () -> domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Client onProviderReady(Consumer<EventDetails> handler) {
|
||||
return on(ProviderEvent.PROVIDER_READY, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Client onProviderConfigurationChanged(Consumer<EventDetails> handler) {
|
||||
return on(ProviderEvent.PROVIDER_CONFIGURATION_CHANGED, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Client onProviderError(Consumer<EventDetails> handler) {
|
||||
return on(ProviderEvent.PROVIDER_ERROR, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Client onProviderStale(Consumer<EventDetails> handler) {
|
||||
return on(ProviderEvent.PROVIDER_STALE, handler);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Client on(ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
openfeatureApi.addHandler(domain, event, handler);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public Client removeHandler(ProviderEvent event, Consumer<EventDetails> handler) {
|
||||
openfeatureApi.removeHandler(domain, event, handler);
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,26 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* Contains information about how the a flag was evaluated, including the resolved value.
|
||||
*
|
||||
* @param <T> the type of the flag being evaluated.
|
||||
*/
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ProviderEvaluation<T> implements BaseEvaluation<T> {
|
||||
T value;
|
||||
String variant;
|
||||
private String reason;
|
||||
ErrorCode errorCode;
|
||||
private String errorMessage;
|
||||
|
||||
@Builder.Default
|
||||
private ImmutableMetadata flagMetadata = ImmutableMetadata.builder().build();
|
||||
}
|
|
@ -0,0 +1,11 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* Provider event types.
|
||||
*/
|
||||
public enum ProviderEvent {
|
||||
PROVIDER_READY,
|
||||
PROVIDER_CONFIGURATION_CHANGED,
|
||||
PROVIDER_ERROR,
|
||||
PROVIDER_STALE;
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
|
||||
/**
|
||||
* The details of a particular event.
|
||||
*/
|
||||
@Data
|
||||
@SuperBuilder(toBuilder = true)
|
||||
public class ProviderEventDetails {
|
||||
private List<String> flagsChanged;
|
||||
private String message;
|
||||
private ImmutableMetadata eventMetadata;
|
||||
private ErrorCode errorCode;
|
||||
}
|
|
@ -0,0 +1,283 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.GeneralError;
|
||||
import dev.openfeature.sdk.exceptions.OpenFeatureError;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
class ProviderRepository {
|
||||
|
||||
private final Map<String, FeatureProviderStateManager> stateManagers = new ConcurrentHashMap<>();
|
||||
private final AtomicReference<FeatureProviderStateManager> defaultStateManger =
|
||||
new AtomicReference<>(new FeatureProviderStateManager(new NoOpProvider()));
|
||||
private final ExecutorService taskExecutor = Executors.newCachedThreadPool(runnable -> {
|
||||
final Thread thread = new Thread(runnable);
|
||||
thread.setDaemon(true);
|
||||
return thread;
|
||||
});
|
||||
private final Object registerStateManagerLock = new Object();
|
||||
private final OpenFeatureAPI openFeatureAPI;
|
||||
|
||||
public ProviderRepository(OpenFeatureAPI openFeatureAPI) {
|
||||
this.openFeatureAPI = openFeatureAPI;
|
||||
}
|
||||
|
||||
FeatureProviderStateManager getFeatureProviderStateManager() {
|
||||
return defaultStateManger.get();
|
||||
}
|
||||
|
||||
FeatureProviderStateManager getFeatureProviderStateManager(String domain) {
|
||||
if (domain == null) {
|
||||
return defaultStateManger.get();
|
||||
}
|
||||
FeatureProviderStateManager fromMap = this.stateManagers.get(domain);
|
||||
if (fromMap == null) {
|
||||
return this.defaultStateManger.get();
|
||||
} else {
|
||||
return fromMap;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the default provider.
|
||||
*/
|
||||
public FeatureProvider getProvider() {
|
||||
return defaultStateManger.get().getProvider();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a provider for a domain. If not found, return the default.
|
||||
*
|
||||
* @param domain The domain to look for.
|
||||
* @return A named {@link FeatureProvider}
|
||||
*/
|
||||
public FeatureProvider getProvider(String domain) {
|
||||
return getFeatureProviderStateManager(domain).getProvider();
|
||||
}
|
||||
|
||||
public ProviderState getProviderState() {
|
||||
return getFeatureProviderStateManager().getState();
|
||||
}
|
||||
|
||||
public ProviderState getProviderState(FeatureProvider featureProvider) {
|
||||
if (featureProvider instanceof FeatureProviderStateManager) {
|
||||
return ((FeatureProviderStateManager) featureProvider).getState();
|
||||
}
|
||||
|
||||
FeatureProviderStateManager defaultProvider = this.defaultStateManger.get();
|
||||
if (defaultProvider.hasSameProvider(featureProvider)) {
|
||||
return defaultProvider.getState();
|
||||
}
|
||||
|
||||
for (FeatureProviderStateManager wrapper : stateManagers.values()) {
|
||||
if (wrapper.hasSameProvider(featureProvider)) {
|
||||
return wrapper.getState();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public ProviderState getProviderState(String domain) {
|
||||
return Optional.ofNullable(domain)
|
||||
.map(this.stateManagers::get)
|
||||
.orElse(this.defaultStateManger.get())
|
||||
.getState();
|
||||
}
|
||||
|
||||
public List<String> getDomainsForProvider(FeatureProvider provider) {
|
||||
return stateManagers.entrySet().stream()
|
||||
.filter(entry -> entry.getValue().hasSameProvider(provider))
|
||||
.map(Map.Entry::getKey)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
public Set<String> getAllBoundDomains() {
|
||||
return stateManagers.keySet();
|
||||
}
|
||||
|
||||
public boolean isDefaultProvider(FeatureProvider provider) {
|
||||
return this.getProvider().equals(provider);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default provider.
|
||||
*/
|
||||
public void setProvider(
|
||||
FeatureProvider provider,
|
||||
Consumer<FeatureProvider> afterSet,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
|
||||
boolean waitForInit) {
|
||||
if (provider == null) {
|
||||
throw new IllegalArgumentException("Provider cannot be null");
|
||||
}
|
||||
prepareAndInitializeProvider(null, provider, afterSet, afterInit, afterShutdown, afterError, waitForInit);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a provider for a domain.
|
||||
*
|
||||
* @param domain The domain to bind the provider to.
|
||||
* @param provider The provider to set.
|
||||
* @param waitForInit When true, wait for initialization to finish, then returns.
|
||||
* Otherwise, initialization happens in the background.
|
||||
*/
|
||||
public void setProvider(
|
||||
String domain,
|
||||
FeatureProvider provider,
|
||||
Consumer<FeatureProvider> afterSet,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
|
||||
boolean waitForInit) {
|
||||
if (provider == null) {
|
||||
throw new IllegalArgumentException("Provider cannot be null");
|
||||
}
|
||||
if (domain == null) {
|
||||
throw new IllegalArgumentException("domain cannot be null");
|
||||
}
|
||||
prepareAndInitializeProvider(domain, provider, afterSet, afterInit, afterShutdown, afterError, waitForInit);
|
||||
}
|
||||
|
||||
private void prepareAndInitializeProvider(
|
||||
String domain,
|
||||
FeatureProvider newProvider,
|
||||
Consumer<FeatureProvider> afterSet,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
|
||||
boolean waitForInit) {
|
||||
final FeatureProviderStateManager newStateManager;
|
||||
final FeatureProviderStateManager oldStateManager;
|
||||
|
||||
synchronized (registerStateManagerLock) {
|
||||
FeatureProviderStateManager existing = getExistingStateManagerForProvider(newProvider);
|
||||
if (existing == null) {
|
||||
newStateManager = new FeatureProviderStateManager(newProvider);
|
||||
// only run afterSet if new provider is not already attached
|
||||
afterSet.accept(newProvider);
|
||||
} else {
|
||||
newStateManager = existing;
|
||||
}
|
||||
|
||||
// provider is set immediately, on this thread
|
||||
oldStateManager = domain != null
|
||||
? this.stateManagers.put(domain, newStateManager)
|
||||
: this.defaultStateManger.getAndSet(newStateManager);
|
||||
}
|
||||
|
||||
if (waitForInit) {
|
||||
initializeProvider(newStateManager, afterInit, afterShutdown, afterError, oldStateManager);
|
||||
} else {
|
||||
taskExecutor.submit(() -> {
|
||||
// initialization happens in a different thread if we're not waiting for it
|
||||
initializeProvider(newStateManager, afterInit, afterShutdown, afterError, oldStateManager);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private FeatureProviderStateManager getExistingStateManagerForProvider(FeatureProvider provider) {
|
||||
for (FeatureProviderStateManager stateManager : stateManagers.values()) {
|
||||
if (stateManager.hasSameProvider(provider)) {
|
||||
return stateManager;
|
||||
}
|
||||
}
|
||||
FeatureProviderStateManager defaultFeatureProviderStateManager = defaultStateManger.get();
|
||||
if (defaultFeatureProviderStateManager.hasSameProvider(provider)) {
|
||||
return defaultFeatureProviderStateManager;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void initializeProvider(
|
||||
FeatureProviderStateManager newManager,
|
||||
Consumer<FeatureProvider> afterInit,
|
||||
Consumer<FeatureProvider> afterShutdown,
|
||||
BiConsumer<FeatureProvider, OpenFeatureError> afterError,
|
||||
FeatureProviderStateManager oldManager) {
|
||||
try {
|
||||
if (ProviderState.NOT_READY.equals(newManager.getState())) {
|
||||
newManager.initialize(openFeatureAPI.getEvaluationContext());
|
||||
afterInit.accept(newManager.getProvider());
|
||||
}
|
||||
shutDownOld(oldManager, afterShutdown);
|
||||
} catch (OpenFeatureError e) {
|
||||
log.error(
|
||||
"Exception when initializing feature provider {}",
|
||||
newManager.getProvider().getClass().getName(),
|
||||
e);
|
||||
afterError.accept(newManager.getProvider(), e);
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Exception when initializing feature provider {}",
|
||||
newManager.getProvider().getClass().getName(),
|
||||
e);
|
||||
afterError.accept(newManager.getProvider(), new GeneralError(e));
|
||||
}
|
||||
}
|
||||
|
||||
private void shutDownOld(FeatureProviderStateManager oldManager, Consumer<FeatureProvider> afterShutdown) {
|
||||
if (oldManager != null && !isStateManagerRegistered(oldManager)) {
|
||||
shutdownProvider(oldManager);
|
||||
afterShutdown.accept(oldManager.getProvider());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper to check if manager is already known (registered).
|
||||
*
|
||||
* @param manager manager to check for registration
|
||||
* @return boolean true if already registered, false otherwise
|
||||
*/
|
||||
private boolean isStateManagerRegistered(FeatureProviderStateManager manager) {
|
||||
return manager != null
|
||||
&& (this.stateManagers.containsValue(manager)
|
||||
|| this.defaultStateManger.get().equals(manager));
|
||||
}
|
||||
|
||||
private void shutdownProvider(FeatureProviderStateManager manager) {
|
||||
if (manager == null) {
|
||||
return;
|
||||
}
|
||||
shutdownProvider(manager.getProvider());
|
||||
}
|
||||
|
||||
private void shutdownProvider(FeatureProvider provider) {
|
||||
taskExecutor.submit(() -> {
|
||||
try {
|
||||
provider.shutdown();
|
||||
} catch (Exception e) {
|
||||
log.error(
|
||||
"Exception when shutting down feature provider {}",
|
||||
provider.getClass().getName(),
|
||||
e);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Shuts down this repository which includes shutting down all FeatureProviders
|
||||
* that are registered,
|
||||
* including the default feature provider.
|
||||
*/
|
||||
public void shutdown() {
|
||||
Stream.concat(Stream.of(this.defaultStateManger.get()), this.stateManagers.values().stream())
|
||||
.distinct()
|
||||
.forEach(this::shutdownProvider);
|
||||
this.stateManagers.clear();
|
||||
taskExecutor.shutdown();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* Indicates the state of the provider.
|
||||
*/
|
||||
public enum ProviderState {
|
||||
READY,
|
||||
NOT_READY,
|
||||
ERROR,
|
||||
STALE,
|
||||
FATAL;
|
||||
|
||||
/**
|
||||
* Returns true if the passed ProviderEvent maps to this ProviderState.
|
||||
*
|
||||
* @param event event to compare
|
||||
* @return boolean if matches.
|
||||
*/
|
||||
boolean matchesEvent(ProviderEvent event) {
|
||||
return this == READY && event == ProviderEvent.PROVIDER_READY
|
||||
|| this == STALE && event == ProviderEvent.PROVIDER_STALE
|
||||
|| this == ERROR && event == ProviderEvent.PROVIDER_ERROR;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* Predefined resolution reasons.
|
||||
*/
|
||||
public enum Reason {
|
||||
DISABLED,
|
||||
SPLIT,
|
||||
TARGETING_MATCH,
|
||||
DEFAULT,
|
||||
UNKNOWN,
|
||||
CACHED,
|
||||
STATIC,
|
||||
ERROR
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* An extension point which can run around flag resolution. They are intended to be used as a way to add custom logic
|
||||
* to the lifecycle of flag evaluation.
|
||||
*
|
||||
* @see Hook
|
||||
*/
|
||||
public interface StringHook extends Hook<String> {
|
||||
|
||||
@Override
|
||||
default boolean supportsFlagValueType(FlagValueType flagValueType) {
|
||||
return FlagValueType.STRING == flagValueType;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,123 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import static dev.openfeature.sdk.Value.objectToValue;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.ValueNotConvertableError;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* {@link Structure} represents a potentially nested object type which is used to represent
|
||||
* structured data.
|
||||
*/
|
||||
@SuppressWarnings("PMD.BeanMembersShouldSerialize")
|
||||
public interface Structure {
|
||||
|
||||
/**
|
||||
* Boolean indicating if this structure is empty.
|
||||
*
|
||||
* @return boolean for emptiness
|
||||
*/
|
||||
boolean isEmpty();
|
||||
|
||||
/**
|
||||
* Get all keys.
|
||||
*
|
||||
* @return the set of keys
|
||||
*/
|
||||
Set<String> keySet();
|
||||
|
||||
/**
|
||||
* Get the value indexed by key.
|
||||
*
|
||||
* @param key String the key.
|
||||
* @return the Value
|
||||
*/
|
||||
Value getValue(String key);
|
||||
|
||||
/**
|
||||
* Get all values, as a map of Values.
|
||||
*
|
||||
* @return all attributes on the structure into a Map
|
||||
*/
|
||||
Map<String, Value> asMap();
|
||||
|
||||
/**
|
||||
* Get all values, as a map of Values.
|
||||
*
|
||||
* @return all attributes on the structure into a Map
|
||||
*/
|
||||
Map<String, Value> asUnmodifiableMap();
|
||||
|
||||
/**
|
||||
* Get all values, with as a map of Object.
|
||||
*
|
||||
* @return all attributes on the structure into a Map
|
||||
*/
|
||||
Map<String, Object> asObjectMap();
|
||||
|
||||
/**
|
||||
* Converts the Value into its equivalent primitive type.
|
||||
*
|
||||
* @param value - Value object to convert
|
||||
* @return an Object containing the primitive type, or null.
|
||||
*/
|
||||
default Object convertValue(Value value) {
|
||||
|
||||
if (value == null || value.isNull()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (value.isBoolean()) {
|
||||
return value.asBoolean();
|
||||
}
|
||||
|
||||
if (value.isNumber() && !value.isNull()) {
|
||||
Number numberValue = (Number) value.asObject();
|
||||
if (numberValue instanceof Double) {
|
||||
return numberValue.doubleValue();
|
||||
} else if (numberValue instanceof Integer) {
|
||||
return numberValue.intValue();
|
||||
}
|
||||
}
|
||||
|
||||
if (value.isString()) {
|
||||
return value.asString();
|
||||
}
|
||||
|
||||
if (value.isInstant()) {
|
||||
return value.asInstant();
|
||||
}
|
||||
|
||||
if (value.isList()) {
|
||||
return value.asList().stream().map(this::convertValue).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
if (value.isStructure()) {
|
||||
Structure s = value.asStructure();
|
||||
return s.asUnmodifiableMap().entrySet().stream()
|
||||
.collect(
|
||||
HashMap::new,
|
||||
(accumulated, entry) -> accumulated.put(entry.getKey(), convertValue(entry.getValue())),
|
||||
HashMap::putAll);
|
||||
}
|
||||
|
||||
throw new ValueNotConvertableError();
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform an object map to a {@link Structure} type.
|
||||
*
|
||||
* @param map map of objects
|
||||
* @return a Structure object in the SDK format
|
||||
*/
|
||||
static Structure mapToStructure(Map<String, Object> map) {
|
||||
return new MutableStructure(map.entrySet().stream()
|
||||
.collect(
|
||||
HashMap::new,
|
||||
(accumulated, entry) -> accumulated.put(entry.getKey(), objectToValue(entry.getValue())),
|
||||
HashMap::putAll));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* The Telemetry class provides constants and methods for creating OpenTelemetry compliant
|
||||
* evaluation events.
|
||||
*/
|
||||
public class Telemetry {
|
||||
|
||||
private Telemetry() {}
|
||||
|
||||
/*
|
||||
The OpenTelemetry compliant event attributes for flag evaluation.
|
||||
Specification: https://opentelemetry.io/docs/specs/semconv/feature-flags/feature-flags-logs/
|
||||
*/
|
||||
public static final String TELEMETRY_KEY = "feature_flag.key";
|
||||
public static final String TELEMETRY_ERROR_CODE = "error.type";
|
||||
public static final String TELEMETRY_VARIANT = "feature_flag.result.variant";
|
||||
public static final String TELEMETRY_VALUE = "feature_flag.result.value";
|
||||
public static final String TELEMETRY_CONTEXT_ID = "feature_flag.context.id";
|
||||
public static final String TELEMETRY_ERROR_MSG = "feature_flag.evaluation.error.message";
|
||||
public static final String TELEMETRY_REASON = "feature_flag.result.reason";
|
||||
public static final String TELEMETRY_PROVIDER = "feature_flag.provider.name";
|
||||
public static final String TELEMETRY_FLAG_SET_ID = "feature_flag.set.id";
|
||||
public static final String TELEMETRY_VERSION = "feature_flag.version";
|
||||
|
||||
// Well-known flag metadata attributes for telemetry events.
|
||||
// Specification: https://openfeature.dev/specification/appendix-d#flag-metadata
|
||||
public static final String TELEMETRY_FLAG_META_CONTEXT_ID = "contextId";
|
||||
public static final String TELEMETRY_FLAG_META_FLAG_SET_ID = "flagSetId";
|
||||
public static final String TELEMETRY_FLAG_META_VERSION = "version";
|
||||
|
||||
public static final String FLAG_EVALUATION_EVENT_NAME = "feature_flag.evaluation";
|
||||
|
||||
/**
|
||||
* Creates an EvaluationEvent using the provided HookContext and ProviderEvaluation.
|
||||
*
|
||||
* @param hookContext the context containing flag evaluation details
|
||||
* @param evaluationDetails the evaluation result from the provider
|
||||
*
|
||||
* @return an EvaluationEvent populated with telemetry data
|
||||
*/
|
||||
public static EvaluationEvent createEvaluationEvent(
|
||||
HookContext<?> hookContext, FlagEvaluationDetails<?> evaluationDetails) {
|
||||
EvaluationEvent.EvaluationEventBuilder evaluationEventBuilder = EvaluationEvent.builder()
|
||||
.name(FLAG_EVALUATION_EVENT_NAME)
|
||||
.attribute(TELEMETRY_KEY, hookContext.getFlagKey())
|
||||
.attribute(TELEMETRY_PROVIDER, hookContext.getProviderMetadata().getName());
|
||||
|
||||
if (evaluationDetails.getReason() != null) {
|
||||
evaluationEventBuilder.attribute(
|
||||
TELEMETRY_REASON, evaluationDetails.getReason().toLowerCase());
|
||||
} else {
|
||||
evaluationEventBuilder.attribute(
|
||||
TELEMETRY_REASON, Reason.UNKNOWN.name().toLowerCase());
|
||||
}
|
||||
|
||||
if (evaluationDetails.getVariant() != null) {
|
||||
evaluationEventBuilder.attribute(TELEMETRY_VARIANT, evaluationDetails.getVariant());
|
||||
} else {
|
||||
evaluationEventBuilder.attribute(TELEMETRY_VALUE, evaluationDetails.getValue());
|
||||
}
|
||||
|
||||
String contextId = evaluationDetails.getFlagMetadata().getString(TELEMETRY_FLAG_META_CONTEXT_ID);
|
||||
if (contextId != null) {
|
||||
evaluationEventBuilder.attribute(TELEMETRY_CONTEXT_ID, contextId);
|
||||
} else {
|
||||
evaluationEventBuilder.attribute(
|
||||
TELEMETRY_CONTEXT_ID, hookContext.getCtx().getTargetingKey());
|
||||
}
|
||||
|
||||
String setID = evaluationDetails.getFlagMetadata().getString(TELEMETRY_FLAG_META_FLAG_SET_ID);
|
||||
if (setID != null) {
|
||||
evaluationEventBuilder.attribute(TELEMETRY_FLAG_SET_ID, setID);
|
||||
}
|
||||
|
||||
String version = evaluationDetails.getFlagMetadata().getString(TELEMETRY_FLAG_META_VERSION);
|
||||
if (version != null) {
|
||||
evaluationEventBuilder.attribute(TELEMETRY_VERSION, version);
|
||||
}
|
||||
|
||||
if (Reason.ERROR.name().equals(evaluationDetails.getReason())) {
|
||||
if (evaluationDetails.getErrorCode() != null) {
|
||||
evaluationEventBuilder.attribute(TELEMETRY_ERROR_CODE, evaluationDetails.getErrorCode());
|
||||
} else {
|
||||
evaluationEventBuilder.attribute(TELEMETRY_ERROR_CODE, ErrorCode.GENERAL);
|
||||
}
|
||||
|
||||
if (evaluationDetails.getErrorMessage() != null) {
|
||||
evaluationEventBuilder.attribute(TELEMETRY_ERROR_MSG, evaluationDetails.getErrorMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return evaluationEventBuilder.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* A {@link ThreadLocalTransactionContextPropagator} is a transactional context propagator
|
||||
* that uses a ThreadLocal to persist a transactional context for the duration of a single thread.
|
||||
*
|
||||
* @see TransactionContextPropagator
|
||||
*/
|
||||
public class ThreadLocalTransactionContextPropagator implements TransactionContextPropagator {
|
||||
|
||||
private final ThreadLocal<EvaluationContext> evaluationContextThreadLocal = new ThreadLocal<>();
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public EvaluationContext getTransactionContext() {
|
||||
return this.evaluationContextThreadLocal.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void setTransactionContext(EvaluationContext evaluationContext) {
|
||||
this.evaluationContextThreadLocal.set(evaluationContext);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* Interface for Tracking events.
|
||||
*/
|
||||
public interface Tracking {
|
||||
/**
|
||||
* Performs tracking of a particular action or application state.
|
||||
*
|
||||
* @param trackingEventName Event name to track
|
||||
* @throws IllegalArgumentException if {@code trackingEventName} is null
|
||||
*/
|
||||
void track(String trackingEventName);
|
||||
|
||||
/**
|
||||
* Performs tracking of a particular action or application state.
|
||||
*
|
||||
* @param trackingEventName Event name to track
|
||||
* @param context Evaluation context used in flag evaluation
|
||||
* @throws IllegalArgumentException if {@code trackingEventName} is null
|
||||
*/
|
||||
void track(String trackingEventName, EvaluationContext context);
|
||||
|
||||
/**
|
||||
* Performs tracking of a particular action or application state.
|
||||
*
|
||||
* @param trackingEventName Event name to track
|
||||
* @param details Data pertinent to a particular tracking event
|
||||
* @throws IllegalArgumentException if {@code trackingEventName} is null
|
||||
*/
|
||||
void track(String trackingEventName, TrackingEventDetails details);
|
||||
|
||||
/**
|
||||
* Performs tracking of a particular action or application state.
|
||||
*
|
||||
* @param trackingEventName Event name to track
|
||||
* @param context Evaluation context used in flag evaluation
|
||||
* @param details Data pertinent to a particular tracking event
|
||||
* @throws IllegalArgumentException if {@code trackingEventName} is null
|
||||
*/
|
||||
void track(String trackingEventName, EvaluationContext context, TrackingEventDetails details);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* Data pertinent to a particular tracking event.
|
||||
*/
|
||||
public interface TrackingEventDetails extends Structure {
|
||||
|
||||
/**
|
||||
* Returns the optional numeric tracking value.
|
||||
*/
|
||||
Optional<Number> getValue();
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
/**
|
||||
* {@link TransactionContextPropagator} is responsible for persisting a transactional context
|
||||
* for the duration of a single transaction.
|
||||
* Examples of potential transaction specific context include: a user id, user agent, IP.
|
||||
* Transaction context is merged with evaluation context prior to flag evaluation.
|
||||
*
|
||||
* <p>
|
||||
* The precedence of merging context can be seen in
|
||||
* <a href=https://openfeature.dev/specification/sections/evaluation-context#requirement-323>the specification</a>.
|
||||
* </p>
|
||||
*/
|
||||
public interface TransactionContextPropagator {
|
||||
|
||||
/**
|
||||
* Returns the currently defined transaction context using the registered transaction
|
||||
* context propagator.
|
||||
*
|
||||
* @return {@link EvaluationContext} The current transaction context
|
||||
*/
|
||||
EvaluationContext getTransactionContext();
|
||||
|
||||
/**
|
||||
* Sets the transaction context.
|
||||
*/
|
||||
void setTransactionContext(EvaluationContext evaluationContext);
|
||||
}
|
|
@ -0,0 +1,319 @@
|
|||
package dev.openfeature.sdk;
|
||||
|
||||
import static dev.openfeature.sdk.Structure.mapToStructure;
|
||||
|
||||
import dev.openfeature.sdk.exceptions.TypeMismatchError;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.SneakyThrows;
|
||||
import lombok.ToString;
|
||||
|
||||
/**
|
||||
* Values serve as a generic return type for structure data from providers.
|
||||
* Providers may deal in JSON, protobuf, XML or some other data-interchange format.
|
||||
* This intermediate representation provides a good medium of exchange.
|
||||
*/
|
||||
@ToString
|
||||
@EqualsAndHashCode
|
||||
@SuppressWarnings({"PMD.BeanMembersShouldSerialize", "checkstyle:MissingJavadocType", "checkstyle:NoFinalizer"})
|
||||
public class Value implements Cloneable {
|
||||
|
||||
private final Object innerObject;
|
||||
|
||||
protected final void finalize() {
|
||||
// DO NOT REMOVE, spotbugs: CT_CONSTRUCTOR_THROW
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new null Value.
|
||||
*/
|
||||
public Value() {
|
||||
this.innerObject = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a new Value with an Object.
|
||||
*
|
||||
* @param value to be wrapped.
|
||||
* @throws InstantiationException if value is not a valid type
|
||||
* (boolean, string, int, double, list, structure, instant)
|
||||
*/
|
||||
public Value(Object value) throws InstantiationException {
|
||||
this.innerObject = value;
|
||||
if (!this.isNull()
|
||||
&& !this.isBoolean()
|
||||
&& !this.isString()
|
||||
&& !this.isNumber()
|
||||
&& !this.isStructure()
|
||||
&& !this.isList()
|
||||
&& !this.isInstant()) {
|
||||
throw new InstantiationException("Invalid value type: " + value.getClass());
|
||||
}
|
||||
}
|
||||
|
||||
public Value(Value value) {
|
||||
this.innerObject = value.innerObject;
|
||||
}
|
||||
|
||||
public Value(Boolean value) {
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
public Value(String value) {
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
public Value(Integer value) {
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
public Value(Double value) {
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
public Value(Structure value) {
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
public Value(List<Value> value) {
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
public Value(Instant value) {
|
||||
this.innerObject = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this Value represents null.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isNull() {
|
||||
return this.innerObject == null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this Value represents a Boolean.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isBoolean() {
|
||||
return this.innerObject instanceof Boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this Value represents a String.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isString() {
|
||||
return this.innerObject instanceof String;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this Value represents a numeric value.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isNumber() {
|
||||
return this.innerObject instanceof Number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this Value represents a Structure.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isStructure() {
|
||||
return this.innerObject instanceof Structure;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this Value represents a List of Values.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isList() {
|
||||
if (!(this.innerObject instanceof List)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
List<?> list = (List<?>) this.innerObject;
|
||||
if (list.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
for (Object obj : list) {
|
||||
if (!(obj instanceof Value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if this Value represents an Instant.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public boolean isInstant() {
|
||||
return this.innerObject instanceof Instant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying Boolean value, or null.
|
||||
*
|
||||
* @return Boolean
|
||||
*/
|
||||
@edu.umd.cs.findbugs.annotations.SuppressFBWarnings(
|
||||
value = "NP_BOOLEAN_RETURN_NULL",
|
||||
justification = "This is not a plain true/false method. It's understood it can return null.")
|
||||
public Boolean asBoolean() {
|
||||
if (this.isBoolean()) {
|
||||
return (Boolean) this.innerObject;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying object.
|
||||
*
|
||||
* @return Object
|
||||
*/
|
||||
public Object asObject() {
|
||||
return this.innerObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying String value, or null.
|
||||
*
|
||||
* @return String
|
||||
*/
|
||||
public String asString() {
|
||||
if (this.isString()) {
|
||||
return (String) this.innerObject;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying numeric value as an Integer, or null.
|
||||
* If the value is not an integer, it will be rounded using Math.round().
|
||||
*
|
||||
* @return Integer
|
||||
*/
|
||||
public Integer asInteger() {
|
||||
if (this.isNumber() && !this.isNull()) {
|
||||
return ((Number) this.innerObject).intValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying numeric value as a Double, or null.
|
||||
*
|
||||
* @return Double
|
||||
*/
|
||||
public Double asDouble() {
|
||||
if (this.isNumber() && !isNull()) {
|
||||
return ((Number) this.innerObject).doubleValue();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying Structure value, or null.
|
||||
*
|
||||
* @return Structure
|
||||
*/
|
||||
public Structure asStructure() {
|
||||
if (this.isStructure()) {
|
||||
return (Structure) this.innerObject;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying List value, or null.
|
||||
*
|
||||
* @return List
|
||||
*/
|
||||
public List<Value> asList() {
|
||||
if (this.isList()) {
|
||||
//noinspection rawtypes,unchecked
|
||||
return (List) this.innerObject;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the underlying Instant value, or null.
|
||||
*
|
||||
* @return Instant
|
||||
*/
|
||||
public Instant asInstant() {
|
||||
if (this.isInstant()) {
|
||||
return (Instant) this.innerObject;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform deep clone of value object.
|
||||
*
|
||||
* @return Value
|
||||
*/
|
||||
@SneakyThrows
|
||||
@Override
|
||||
protected Value clone() {
|
||||
if (this.isList()) {
|
||||
List<Value> copy = this.asList().stream().map(Value::new).collect(Collectors.toList());
|
||||
return new Value(copy);
|
||||
}
|
||||
if (this.isStructure()) {
|
||||
return new Value(new ImmutableStructure(this.asStructure().asUnmodifiableMap()));
|
||||
}
|
||||
if (this.isInstant()) {
|
||||
Instant copy = Instant.ofEpochMilli(this.asInstant().toEpochMilli());
|
||||
return new Value(copy);
|
||||
}
|
||||
return new Value(this.asObject());
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrap an object into a Value.
|
||||
*
|
||||
* @param object the object to wrap
|
||||
* @return the wrapped object
|
||||
*/
|
||||
public static Value objectToValue(Object object) {
|
||||
if (object instanceof Value) {
|
||||
return (Value) object;
|
||||
} else if (object == null) {
|
||||
return new Value();
|
||||
} else if (object instanceof String) {
|
||||
return new Value((String) object);
|
||||
} else if (object instanceof Boolean) {
|
||||
return new Value((Boolean) object);
|
||||
} else if (object instanceof Integer) {
|
||||
return new Value((Integer) object);
|
||||
} else if (object instanceof Double) {
|
||||
return new Value((Double) object);
|
||||
} else if (object instanceof Structure) {
|
||||
return new Value((Structure) object);
|
||||
} else if (object instanceof List) {
|
||||
return new Value(
|
||||
((List<Object>) object).stream().map(o -> objectToValue(o)).collect(Collectors.toList()));
|
||||
} else if (object instanceof Instant) {
|
||||
return new Value((Instant) object);
|
||||
} else if (object instanceof Map) {
|
||||
return new Value(mapToStructure((Map<String, Object>) object));
|
||||
} else {
|
||||
throw new TypeMismatchError("Flag value " + object + " had unexpected type " + object.getClass() + ".");
|
||||
}
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue