XXE
37 | * OWASP CheatSheet
38 | */
39 | public class XMLInputFactorySecurity {
40 |
41 | private XMLInputFactorySecurity() {}
42 |
43 | public static XMLInputFactory hardenFactory(final XMLInputFactory factory) {
44 | Objects.requireNonNull(factory);
45 | // disable XML external entity (XXE) processing
46 | factory.setProperty(XMLInputFactory.IS_SUPPORTING_EXTERNAL_ENTITIES, false);
47 | factory.setProperty(XMLInputFactory.SUPPORT_DTD, false);
48 | return factory;
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/internal/stream/AbstractAutoCloseStream.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024, Apptastic Software
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.apptasticsoftware.rssreader.internal.stream;
25 |
26 | import java.util.Objects;
27 | import java.util.concurrent.atomic.AtomicBoolean;
28 | import java.util.function.*;
29 | import java.util.stream.*;
30 |
31 | @SuppressWarnings("javaarchitecture:S7027")
32 | public class AbstractAutoCloseStream> implements AutoCloseable {
33 | private final S stream;
34 | private final AtomicBoolean isClosed;
35 |
36 | AbstractAutoCloseStream(S stream) {
37 | this.stream = Objects.requireNonNull(stream);
38 | this.isClosed = new AtomicBoolean();
39 | }
40 |
41 | protected S stream() {
42 | return stream;
43 | }
44 |
45 | @Override
46 | public void close() {
47 | if (isClosed.compareAndSet(false,true)) {
48 | stream().close();
49 | }
50 | }
51 |
52 | R autoClose(Function function) {
53 | try (S s = stream()) {
54 | return function.apply(s);
55 | }
56 | }
57 |
58 | Stream asAutoCloseStream(Stream stream) {
59 | return asAutoCloseStream(stream, AutoCloseStream::new);
60 | }
61 |
62 | IntStream asAutoCloseStream(IntStream stream) {
63 | return asAutoCloseStream(stream, AutoCloseIntStream::new);
64 | }
65 |
66 | LongStream asAutoCloseStream(LongStream stream) {
67 | return asAutoCloseStream(stream, AutoCloseLongStream::new);
68 | }
69 |
70 | DoubleStream asAutoCloseStream(DoubleStream stream) {
71 | return asAutoCloseStream(stream, AutoCloseDoubleStream::new);
72 | }
73 |
74 | private U asAutoCloseStream(U stream, UnaryOperator wrapper) {
75 | if (stream instanceof AbstractAutoCloseStream) {
76 | return stream;
77 | }
78 | return wrapper.apply(stream);
79 | }
80 |
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/internal/stream/AutoCloseDoubleStream.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024, Apptastic Software
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.apptasticsoftware.rssreader.internal.stream;
25 |
26 | import java.util.DoubleSummaryStatistics;
27 | import java.util.OptionalDouble;
28 | import java.util.PrimitiveIterator;
29 | import java.util.Spliterator;
30 | import java.util.function.*;
31 | import java.util.stream.DoubleStream;
32 | import java.util.stream.IntStream;
33 | import java.util.stream.LongStream;
34 | import java.util.stream.Stream;
35 |
36 | public class AutoCloseDoubleStream extends AbstractAutoCloseStream implements DoubleStream {
37 |
38 | AutoCloseDoubleStream(DoubleStream stream) {
39 | super(stream);
40 | }
41 |
42 | @Override
43 | public DoubleStream filter(DoublePredicate predicate) {
44 | return asAutoCloseStream(stream().filter(predicate));
45 | }
46 |
47 | @Override
48 | public DoubleStream map(DoubleUnaryOperator mapper) {
49 | return asAutoCloseStream(stream().map(mapper));
50 | }
51 |
52 | @Override
53 | public Stream mapToObj(DoubleFunction extends U> mapper) {
54 | return asAutoCloseStream(stream().mapToObj(mapper));
55 | }
56 |
57 | @Override
58 | public IntStream mapToInt(DoubleToIntFunction mapper) {
59 | return asAutoCloseStream(stream().mapToInt(mapper));
60 | }
61 |
62 | @Override
63 | public LongStream mapToLong(DoubleToLongFunction mapper) {
64 | return asAutoCloseStream(stream().mapToLong(mapper));
65 | }
66 |
67 | @Override
68 | public DoubleStream flatMap(DoubleFunction extends DoubleStream> mapper) {
69 | return asAutoCloseStream(stream().flatMap(mapper));
70 | }
71 |
72 | @Override
73 | public DoubleStream distinct() {
74 | return asAutoCloseStream(stream().distinct());
75 | }
76 |
77 | @Override
78 | public DoubleStream sorted() {
79 | return asAutoCloseStream(stream().sorted());
80 | }
81 |
82 | @SuppressWarnings("java:S3864")
83 | @Override
84 | public DoubleStream peek(DoubleConsumer action) {
85 | return asAutoCloseStream(stream().peek(action));
86 | }
87 |
88 | @Override
89 | public DoubleStream limit(long maxSize) {
90 | return asAutoCloseStream(stream().limit(maxSize));
91 | }
92 |
93 | @Override
94 | public DoubleStream skip(long n) {
95 | return asAutoCloseStream(stream().skip(n));
96 | }
97 |
98 | @Override
99 | public void forEach(DoubleConsumer action) {
100 | autoClose(stream -> {
101 | stream.forEach(action);
102 | return null;
103 | });
104 | }
105 |
106 | @Override
107 | public void forEachOrdered(DoubleConsumer action) {
108 | autoClose(stream -> {
109 | stream.forEachOrdered(action);
110 | return null;
111 | });
112 | }
113 |
114 | @Override
115 | public double[] toArray() {
116 | return autoClose(DoubleStream::toArray);
117 | }
118 |
119 | @Override
120 | public double reduce(double identity, DoubleBinaryOperator op) {
121 | return autoClose(stream -> stream.reduce(identity, op));
122 | }
123 |
124 | @Override
125 | public OptionalDouble reduce(DoubleBinaryOperator op) {
126 | return autoClose(stream -> stream.reduce(op));
127 | }
128 |
129 | @Override
130 | public R collect(Supplier supplier, ObjDoubleConsumer accumulator, BiConsumer combiner) {
131 | return autoClose(stream -> stream.collect(supplier, accumulator, combiner));
132 | }
133 |
134 | @Override
135 | public double sum() {
136 | return autoClose(DoubleStream::sum);
137 | }
138 |
139 | @Override
140 | public OptionalDouble min() {
141 | return autoClose(DoubleStream::min);
142 | }
143 |
144 | @Override
145 | public OptionalDouble max() {
146 | return autoClose(DoubleStream::max);
147 | }
148 |
149 | @Override
150 | public long count() {
151 | return autoClose(DoubleStream::count);
152 | }
153 |
154 | @Override
155 | public OptionalDouble average() {
156 | return autoClose(DoubleStream::average);
157 | }
158 |
159 | @Override
160 | public DoubleSummaryStatistics summaryStatistics() {
161 | return autoClose(DoubleStream::summaryStatistics);
162 | }
163 |
164 | @Override
165 | public boolean anyMatch(DoublePredicate predicate) {
166 | return autoClose(stream -> stream.anyMatch(predicate));
167 | }
168 |
169 | @Override
170 | public boolean allMatch(DoublePredicate predicate) {
171 | return autoClose(stream -> stream.allMatch(predicate));
172 | }
173 |
174 | @Override
175 | public boolean noneMatch(DoublePredicate predicate) {
176 | return autoClose(stream -> stream.noneMatch(predicate));
177 | }
178 |
179 | @Override
180 | public OptionalDouble findFirst() {
181 | return autoClose(DoubleStream::findFirst);
182 | }
183 |
184 | @Override
185 | public OptionalDouble findAny() {
186 | return autoClose(DoubleStream::findAny);
187 | }
188 |
189 | @Override
190 | public Stream boxed() {
191 | return asAutoCloseStream(stream().boxed());
192 | }
193 |
194 | @Override
195 | public DoubleStream sequential() {
196 | return asAutoCloseStream(stream().sequential());
197 | }
198 |
199 | @Override
200 | public DoubleStream parallel() {
201 | return asAutoCloseStream(stream().parallel());
202 | }
203 |
204 | @Override
205 | public PrimitiveIterator.OfDouble iterator() {
206 | return stream().iterator();
207 | }
208 |
209 | @Override
210 | public Spliterator.OfDouble spliterator() {
211 | return stream().spliterator();
212 | }
213 |
214 | @Override
215 | public boolean isParallel() {
216 | return stream().isParallel();
217 | }
218 |
219 | @Override
220 | public DoubleStream unordered() {
221 | return asAutoCloseStream(stream().unordered());
222 | }
223 |
224 | @Override
225 | public DoubleStream onClose(Runnable closeHandler) {
226 | return asAutoCloseStream(stream().onClose(closeHandler));
227 | }
228 | }
229 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/internal/stream/AutoCloseIntStream.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024, Apptastic Software
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.apptasticsoftware.rssreader.internal.stream;
25 |
26 | import java.util.*;
27 | import java.util.function.*;
28 | import java.util.stream.DoubleStream;
29 | import java.util.stream.IntStream;
30 | import java.util.stream.LongStream;
31 | import java.util.stream.Stream;
32 |
33 | public class AutoCloseIntStream extends AbstractAutoCloseStream implements IntStream {
34 |
35 | AutoCloseIntStream(IntStream stream) {
36 | super(stream);
37 | }
38 |
39 | @Override
40 | public IntStream filter(IntPredicate predicate) {
41 | return asAutoCloseStream(stream().filter(predicate));
42 | }
43 |
44 | @Override
45 | public IntStream map(IntUnaryOperator mapper) {
46 | return asAutoCloseStream(stream().map(mapper));
47 | }
48 |
49 | @Override
50 | public Stream mapToObj(IntFunction extends U> mapper) {
51 | return asAutoCloseStream(stream().mapToObj(mapper));
52 | }
53 |
54 | @Override
55 | public LongStream mapToLong(IntToLongFunction mapper) {
56 | return asAutoCloseStream(stream().mapToLong(mapper));
57 | }
58 |
59 | @Override
60 | public DoubleStream mapToDouble(IntToDoubleFunction mapper) {
61 | return asAutoCloseStream(stream().mapToDouble(mapper));
62 | }
63 |
64 | @Override
65 | public IntStream flatMap(IntFunction extends IntStream> mapper) {
66 | return asAutoCloseStream(stream().flatMap(mapper));
67 | }
68 |
69 | @Override
70 | public IntStream distinct() {
71 | return asAutoCloseStream(stream().distinct());
72 | }
73 |
74 | @Override
75 | public IntStream sorted() {
76 | return asAutoCloseStream(stream().sorted());
77 | }
78 |
79 | @SuppressWarnings("java:S3864")
80 | @Override
81 | public IntStream peek(IntConsumer action) {
82 | return asAutoCloseStream(stream().peek(action));
83 | }
84 |
85 | @Override
86 | public IntStream limit(long maxSize) {
87 | return asAutoCloseStream(stream().limit(maxSize));
88 | }
89 |
90 | @Override
91 | public IntStream skip(long n) {
92 | return asAutoCloseStream(stream().skip(n));
93 | }
94 |
95 | @Override
96 | public void forEach(IntConsumer action) {
97 | autoClose(stream -> {
98 | stream.forEach(action);
99 | return null;
100 | });
101 | }
102 |
103 | @Override
104 | public void forEachOrdered(IntConsumer action) {
105 | autoClose(stream -> {
106 | stream.forEachOrdered(action);
107 | return null;
108 | });
109 | }
110 |
111 | @Override
112 | public int[] toArray() {
113 | return autoClose(IntStream::toArray);
114 | }
115 |
116 | @Override
117 | public int reduce(int identity, IntBinaryOperator op) {
118 | return autoClose(stream -> stream.reduce(identity, op));
119 | }
120 |
121 | @Override
122 | public OptionalInt reduce(IntBinaryOperator op) {
123 | return autoClose(stream -> stream.reduce(op));
124 | }
125 |
126 | @Override
127 | public R collect(Supplier supplier, ObjIntConsumer accumulator, BiConsumer combiner) {
128 | return autoClose(stream -> stream.collect(supplier, accumulator, combiner));
129 | }
130 |
131 | @Override
132 | public int sum() {
133 | return autoClose(IntStream::sum);
134 | }
135 |
136 | @Override
137 | public OptionalInt min() {
138 | return autoClose(IntStream::min);
139 | }
140 |
141 | @Override
142 | public OptionalInt max() {
143 | return autoClose(IntStream::max);
144 | }
145 |
146 | @Override
147 | public long count() {
148 | return autoClose(IntStream::count);
149 | }
150 |
151 | @Override
152 | public OptionalDouble average() {
153 | return autoClose(IntStream::average);
154 | }
155 |
156 | @Override
157 | public IntSummaryStatistics summaryStatistics() {
158 | return autoClose(IntStream::summaryStatistics);
159 | }
160 |
161 | @Override
162 | public boolean anyMatch(IntPredicate predicate) {
163 | return autoClose(stream -> stream.anyMatch(predicate));
164 | }
165 |
166 | @Override
167 | public boolean allMatch(IntPredicate predicate) {
168 | return autoClose(stream -> stream.allMatch(predicate));
169 | }
170 |
171 | @Override
172 | public boolean noneMatch(IntPredicate predicate) {
173 | return autoClose(stream -> stream.noneMatch(predicate));
174 | }
175 |
176 | @Override
177 | public OptionalInt findFirst() {
178 | return autoClose(IntStream::findFirst);
179 | }
180 |
181 | @Override
182 | public OptionalInt findAny() {
183 | return autoClose(IntStream::findAny);
184 | }
185 |
186 | @Override
187 | public LongStream asLongStream() {
188 | return asAutoCloseStream(stream().asLongStream());
189 | }
190 |
191 | @Override
192 | public DoubleStream asDoubleStream() {
193 | return asAutoCloseStream(stream().asDoubleStream());
194 | }
195 |
196 | @Override
197 | public Stream boxed() {
198 | return asAutoCloseStream(stream().boxed());
199 | }
200 |
201 | @Override
202 | public IntStream sequential() {
203 | return asAutoCloseStream(stream().sequential());
204 | }
205 |
206 | @Override
207 | public IntStream parallel() {
208 | return asAutoCloseStream(stream().parallel());
209 | }
210 |
211 | @Override
212 | public PrimitiveIterator.OfInt iterator() {
213 | return stream().iterator();
214 | }
215 |
216 | @Override
217 | public Spliterator.OfInt spliterator() {
218 | return stream().spliterator();
219 | }
220 |
221 | @Override
222 | public boolean isParallel() {
223 | return stream().isParallel();
224 | }
225 |
226 | @Override
227 | public IntStream unordered() {
228 | return asAutoCloseStream(stream().unordered());
229 | }
230 |
231 | @Override
232 | public IntStream onClose(Runnable closeHandler) {
233 | return asAutoCloseStream(stream().onClose(closeHandler));
234 | }
235 | }
236 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/internal/stream/AutoCloseLongStream.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024, Apptastic Software
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.apptasticsoftware.rssreader.internal.stream;
25 |
26 | import java.util.*;
27 | import java.util.function.*;
28 | import java.util.stream.DoubleStream;
29 | import java.util.stream.IntStream;
30 | import java.util.stream.LongStream;
31 | import java.util.stream.Stream;
32 |
33 | public class AutoCloseLongStream extends AbstractAutoCloseStream implements LongStream {
34 |
35 | AutoCloseLongStream(LongStream stream) {
36 | super(stream);
37 | }
38 |
39 | @Override
40 | public LongStream filter(LongPredicate predicate) {
41 | return asAutoCloseStream(stream().filter(predicate));
42 | }
43 |
44 | @Override
45 | public LongStream map(LongUnaryOperator mapper) {
46 | return asAutoCloseStream(stream().map(mapper));
47 | }
48 |
49 | @Override
50 | public Stream mapToObj(LongFunction extends U> mapper) {
51 | return asAutoCloseStream(stream().mapToObj(mapper));
52 | }
53 |
54 | @Override
55 | public IntStream mapToInt(LongToIntFunction mapper) {
56 | return asAutoCloseStream(stream().mapToInt(mapper));
57 | }
58 |
59 | @Override
60 | public DoubleStream mapToDouble(LongToDoubleFunction mapper) {
61 | return asAutoCloseStream(stream().mapToDouble(mapper));
62 | }
63 |
64 | @Override
65 | public LongStream flatMap(LongFunction extends LongStream> mapper) {
66 | return asAutoCloseStream(stream().flatMap(mapper));
67 | }
68 |
69 | @Override
70 | public LongStream distinct() {
71 | return asAutoCloseStream(stream().distinct());
72 | }
73 |
74 | @Override
75 | public LongStream sorted() {
76 | return asAutoCloseStream(stream().sorted());
77 | }
78 |
79 | @SuppressWarnings("java:S3864")
80 | @Override
81 | public LongStream peek(LongConsumer action) {
82 | return asAutoCloseStream(stream().peek(action));
83 | }
84 |
85 | @Override
86 | public LongStream limit(long maxSize) {
87 | return asAutoCloseStream(stream().limit(maxSize));
88 | }
89 |
90 | @Override
91 | public LongStream skip(long n) {
92 | return asAutoCloseStream(stream().skip(n));
93 | }
94 |
95 | @Override
96 | public void forEach(LongConsumer action) {
97 | autoClose(stream -> {
98 | stream.forEach(action);
99 | return null;
100 | });
101 | }
102 |
103 | @Override
104 | public void forEachOrdered(LongConsumer action) {
105 | autoClose(stream -> {
106 | stream.forEachOrdered(action);
107 | return null;
108 | });
109 | }
110 |
111 | @Override
112 | public long[] toArray() {
113 | return autoClose(LongStream::toArray);
114 | }
115 |
116 | @Override
117 | public long reduce(long identity, LongBinaryOperator op) {
118 | return autoClose(stream -> stream.reduce(identity, op));
119 | }
120 |
121 | @Override
122 | public OptionalLong reduce(LongBinaryOperator op) {
123 | return autoClose(stream -> stream.reduce(op));
124 | }
125 |
126 | @Override
127 | public R collect(Supplier supplier, ObjLongConsumer accumulator, BiConsumer combiner) {
128 | return autoClose(stream -> stream.collect(supplier, accumulator, combiner));
129 | }
130 |
131 | @Override
132 | public long sum() {
133 | return autoClose(LongStream::sum);
134 | }
135 |
136 | @Override
137 | public OptionalLong min() {
138 | return autoClose(LongStream::min);
139 | }
140 |
141 | @Override
142 | public OptionalLong max() {
143 | return autoClose(LongStream::max);
144 | }
145 |
146 | @Override
147 | public long count() {
148 | return autoClose(LongStream::count);
149 | }
150 |
151 | @Override
152 | public OptionalDouble average() {
153 | return autoClose(LongStream::average);
154 | }
155 |
156 | @Override
157 | public LongSummaryStatistics summaryStatistics() {
158 | return autoClose(LongStream::summaryStatistics);
159 | }
160 |
161 | @Override
162 | public boolean anyMatch(LongPredicate predicate) {
163 | return autoClose(stream -> stream.anyMatch(predicate));
164 | }
165 |
166 | @Override
167 | public boolean allMatch(LongPredicate predicate) {
168 | return autoClose(stream -> stream.allMatch(predicate));
169 | }
170 |
171 | @Override
172 | public boolean noneMatch(LongPredicate predicate) {
173 | return autoClose(stream -> stream.noneMatch(predicate));
174 | }
175 |
176 | @Override
177 | public OptionalLong findFirst() {
178 | return autoClose(LongStream::findFirst);
179 | }
180 |
181 | @Override
182 | public OptionalLong findAny() {
183 | return autoClose(LongStream::findAny);
184 | }
185 |
186 | @Override
187 | public DoubleStream asDoubleStream() {
188 | return asAutoCloseStream(stream().asDoubleStream());
189 | }
190 |
191 | @Override
192 | public Stream boxed() {
193 | return asAutoCloseStream(stream().boxed());
194 | }
195 |
196 | @Override
197 | public LongStream sequential() {
198 | return asAutoCloseStream(stream().sequential());
199 | }
200 |
201 | @Override
202 | public LongStream parallel() {
203 | return asAutoCloseStream(stream().parallel());
204 | }
205 |
206 | @Override
207 | public PrimitiveIterator.OfLong iterator() {
208 | return stream().iterator();
209 | }
210 |
211 | @Override
212 | public Spliterator.OfLong spliterator() {
213 | return stream().spliterator();
214 | }
215 |
216 | @Override
217 | public boolean isParallel() {
218 | return stream().isParallel();
219 | }
220 |
221 | @Override
222 | public LongStream unordered() {
223 | return asAutoCloseStream(stream().unordered());
224 | }
225 |
226 | @Override
227 | public LongStream onClose(Runnable closeHandler) {
228 | return asAutoCloseStream(stream().onClose(closeHandler));
229 | }
230 |
231 | }
232 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/internal/stream/AutoCloseStream.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2024, Apptastic Software
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.apptasticsoftware.rssreader.internal.stream;
25 |
26 | import java.util.*;
27 | import java.util.function.*;
28 | import java.util.stream.*;
29 |
30 | /**
31 | * A Stream that automatically calls its {@link #close()} method after a terminating operation, such as limit(), forEach(), or collect(), has been executed.
32 | *
33 | * This class is useful for working with streams that have resources that need to be closed, such as file streams or network connections.
34 | *
35 | * If the {@link #close()} method is called manually, the stream will be closed and any subsequent operations will throw an {@link IllegalStateException}.
36 | *
37 | * The {@link #iterator()} and {@link #spliterator()} methods are not supported by this class.
38 | */
39 | @SuppressWarnings("javaarchitecture:S7027")
40 | public class AutoCloseStream extends AbstractAutoCloseStream> implements Stream {
41 |
42 | AutoCloseStream(Stream stream) {
43 | super(stream);
44 | }
45 |
46 | /**
47 | * Creates a new AutoCloseStream from the given stream.
48 | * @param stream the stream to wrap
49 | * @return a new AutoCloseStream
50 | */
51 | public static AutoCloseStream of(Stream stream) {
52 | Objects.requireNonNull(stream);
53 | return new AutoCloseStream<>(stream);
54 | }
55 |
56 | @Override
57 | public Stream filter(Predicate super T> predicate) {
58 | return asAutoCloseStream(stream().filter(predicate));
59 | }
60 |
61 | @Override
62 | public Stream map(Function super T, ? extends R> mapper) {
63 | return asAutoCloseStream(stream().map(mapper));
64 | }
65 |
66 | @Override
67 | public IntStream mapToInt(ToIntFunction super T> mapper) {
68 | return asAutoCloseStream(stream().mapToInt(mapper));
69 | }
70 |
71 | @Override
72 | public LongStream mapToLong(ToLongFunction super T> mapper) {
73 | return asAutoCloseStream(stream().mapToLong(mapper));
74 | }
75 |
76 | @Override
77 | public DoubleStream mapToDouble(ToDoubleFunction super T> mapper) {
78 | return asAutoCloseStream(stream().mapToDouble(mapper));
79 | }
80 |
81 | @Override
82 | public Stream flatMap(Function super T, ? extends Stream extends R>> mapper) {
83 | return asAutoCloseStream(stream().flatMap(mapper));
84 | }
85 |
86 | @Override
87 | public IntStream flatMapToInt(Function super T, ? extends IntStream> mapper) {
88 | return asAutoCloseStream(stream().flatMapToInt(mapper));
89 | }
90 |
91 | @Override
92 | public LongStream flatMapToLong(Function super T, ? extends LongStream> mapper) {
93 | return asAutoCloseStream(stream().flatMapToLong(mapper));
94 | }
95 |
96 | @Override
97 | public DoubleStream flatMapToDouble(Function super T, ? extends DoubleStream> mapper) {
98 | return asAutoCloseStream(stream().flatMapToDouble(mapper));
99 | }
100 |
101 | @Override
102 | public Stream distinct() {
103 | return asAutoCloseStream(stream().distinct());
104 | }
105 |
106 | @Override
107 | public Stream sorted() {
108 | return asAutoCloseStream(stream().sorted());
109 | }
110 |
111 | @Override
112 | public Stream sorted(Comparator super T> comparator) {
113 | return asAutoCloseStream(stream().sorted(comparator));
114 | }
115 |
116 | @SuppressWarnings("java:S3864")
117 | @Override
118 | public Stream peek(Consumer super T> action) {
119 | return asAutoCloseStream(stream().peek(action));
120 | }
121 |
122 | @Override
123 | public Stream limit(long maxSize) {
124 | return asAutoCloseStream(stream().limit(maxSize));
125 | }
126 |
127 | @Override
128 | public Stream skip(long n) {
129 | return asAutoCloseStream(stream().skip(n));
130 | }
131 |
132 | @Override
133 | public void forEach(Consumer super T> action) {
134 | autoClose(stream -> {
135 | stream.forEach(action);
136 | return null;
137 | });
138 | }
139 |
140 | @Override
141 | public void forEachOrdered(Consumer super T> action) {
142 | autoClose(stream -> {
143 | stream.forEachOrdered(action);
144 | return null;
145 | });
146 | }
147 |
148 | @Override
149 | public Object[] toArray() {
150 | return autoClose(Stream::toArray);
151 | }
152 |
153 | @Override
154 | public A[] toArray(IntFunction generator) {
155 | return autoClose(stream -> stream.toArray(generator));
156 | }
157 |
158 | @Override
159 | public T reduce(T identity, BinaryOperator accumulator) {
160 | return autoClose(stream -> stream.reduce(identity, accumulator));
161 | }
162 |
163 | @Override
164 | public Optional reduce(BinaryOperator accumulator) {
165 | return autoClose(stream -> stream.reduce(accumulator));
166 | }
167 |
168 | @Override
169 | public U reduce(U identity, BiFunction accumulator, BinaryOperator combiner) {
170 | return autoClose(stream -> stream.reduce(identity, accumulator, combiner));
171 | }
172 |
173 | @Override
174 | public R collect(Supplier supplier, BiConsumer accumulator, BiConsumer combiner) {
175 | return autoClose(stream -> stream.collect(supplier, accumulator, combiner));
176 | }
177 |
178 | @Override
179 | public R collect(Collector super T, A, R> collector) {
180 | return autoClose(stream -> stream.collect(collector));
181 | }
182 |
183 | @Override
184 | public Optional min(Comparator super T> comparator) {
185 | return autoClose(stream -> stream.min(comparator));
186 | }
187 |
188 | @Override
189 | public Optional max(Comparator super T> comparator) {
190 | return autoClose(stream -> stream.max(comparator));
191 | }
192 |
193 | @Override
194 | public long count() {
195 | return autoClose(Stream::count);
196 | }
197 |
198 | @Override
199 | public boolean anyMatch(Predicate super T> predicate) {
200 | return autoClose(stream -> stream.anyMatch(predicate));
201 | }
202 |
203 | @Override
204 | public boolean allMatch(Predicate super T> predicate) {
205 | return autoClose(stream -> stream.allMatch(predicate));
206 | }
207 |
208 | @Override
209 | public boolean noneMatch(Predicate super T> predicate) {
210 | return autoClose(stream -> stream.noneMatch(predicate));
211 | }
212 |
213 | @Override
214 | public Optional findFirst() {
215 | return autoClose(Stream::findFirst);
216 | }
217 |
218 | @Override
219 | public Optional findAny() {
220 | return autoClose(Stream::findAny);
221 | }
222 |
223 | @Override
224 | public Iterator iterator() {
225 | return stream().iterator();
226 | }
227 |
228 | @Override
229 | public Spliterator spliterator() {
230 | return stream().spliterator();
231 | }
232 |
233 | @Override
234 | public boolean isParallel() {
235 | return stream().isParallel();
236 | }
237 |
238 | @Override
239 | public Stream sequential() {
240 | return asAutoCloseStream(stream().sequential());
241 | }
242 |
243 | @Override
244 | public Stream parallel() {
245 | return asAutoCloseStream(stream().parallel());
246 | }
247 |
248 | @Override
249 | public Stream unordered() {
250 | return asAutoCloseStream(stream().unordered());
251 | }
252 |
253 | @Override
254 | public Stream onClose(Runnable closeHandler) {
255 | return asAutoCloseStream(stream().onClose(closeHandler));
256 | }
257 | }
258 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/module/itunes/ItunesOwner.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2022, Apptastic Software
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.apptasticsoftware.rssreader.module.itunes;
25 |
26 | import java.util.Objects;
27 | import java.util.Optional;
28 |
29 | /**
30 | * Class representing the Itunes owner.
31 | */
32 | public class ItunesOwner {
33 | private String name;
34 | private String email;
35 |
36 |
37 | /**
38 | * Get the name
39 | * @return name
40 | */
41 | public Optional getName() {
42 | return Optional.ofNullable(name);
43 | }
44 |
45 | /**
46 | * Set the name
47 | * @param name name
48 | */
49 | public void setName(String name) {
50 | this.name = name;
51 | }
52 |
53 | /**
54 | * Get the email
55 | * @return email
56 | */
57 | public String getEmail() {
58 | return email;
59 | }
60 |
61 | /**
62 | * Set the email
63 | * @param email email
64 | */
65 | public void setEmail(String email) {
66 | this.email = email;
67 | }
68 |
69 | @Override
70 | public boolean equals(Object o) {
71 | if (this == o) return true;
72 | if (o == null || getClass() != o.getClass()) return false;
73 | ItunesOwner that = (ItunesOwner) o;
74 | return Objects.equals(getName(), that.getName()) && Objects.equals(getEmail(), that.getEmail());
75 | }
76 |
77 | @Override
78 | public int hashCode() {
79 | return Objects.hash(getName(), getEmail());
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/module/itunes/ItunesRssReader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2022, Apptastic Software
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.apptasticsoftware.rssreader.module.itunes;
25 |
26 | import com.apptasticsoftware.rssreader.AbstractRssReader;
27 | import com.apptasticsoftware.rssreader.DateTimeParser;
28 |
29 | import java.net.http.HttpClient;
30 |
31 | import static com.apptasticsoftware.rssreader.util.Mapper.mapBoolean;
32 | import static com.apptasticsoftware.rssreader.util.Mapper.mapInteger;
33 |
34 | /**
35 | * Class for reading podcast (itunes) feeds.
36 | */
37 | public class ItunesRssReader extends AbstractRssReader {
38 |
39 | /**
40 | * Constructor
41 | */
42 | public ItunesRssReader() {
43 | super();
44 | }
45 |
46 | /**
47 | * Constructor
48 | * @param httpClient http client
49 | */
50 | public ItunesRssReader(HttpClient httpClient) {
51 | super(httpClient);
52 | }
53 |
54 | @Override
55 | protected void registerChannelTags() {
56 | super.registerChannelTags();
57 | addChannelExtension("itunes:explicit", (i, v) -> mapBoolean(v, i::setItunesExplicit));
58 | addChannelExtension("itunes:author", ItunesChannel::setItunesAuthor);
59 |
60 | addChannelExtension("itunes:name", (i, v) -> {
61 | if (i.getItunesOwner().isEmpty())
62 | i.setItunesOwner(new ItunesOwner());
63 | i.getItunesOwner().ifPresent(a -> a.setName(v));
64 | });
65 |
66 | addChannelExtension("itunes:email", (i, v) -> {
67 | if (i.getItunesOwner().isEmpty())
68 | i.setItunesOwner(new ItunesOwner());
69 | i.getItunesOwner().ifPresent(a -> a.setEmail(v));
70 | });
71 |
72 | addChannelExtension("itunes:title", ItunesChannel::setItunesTitle);
73 | addChannelExtension("itunes:subtitle", ItunesChannel::setItunesSubtitle);
74 | addChannelExtension("itunes:summary", ItunesChannel::setItunesSummary);
75 | addChannelExtension("itunes:type", ItunesChannel::setItunesType);
76 | addChannelExtension("itunes:new-feed-url", ItunesChannel::setItunesNewFeedUrl);
77 | addChannelExtension("itunes:block", (i, v) -> mapBoolean(v, i::setItunesBlock));
78 | addChannelExtension("itunes:complete", (i, v) -> mapBoolean(v, i::setItunesComplete));
79 | }
80 |
81 | @Override
82 | protected void registerChannelAttributes() {
83 | super.registerChannelAttributes();
84 | addChannelExtension("itunes:image", "href", ItunesChannel::setItunesImage);
85 | addChannelExtension("itunes:category", "text", ItunesChannel::addItunesCategory);
86 | }
87 |
88 | @Override
89 | protected void registerItemTags() {
90 | super.registerItemTags();
91 | addItemExtension("itunes:duration", ItunesItem::setItunesDuration);
92 | addItemExtension("itunes:explicit", (i, v) -> mapBoolean(v, i::setItunesExplicit));
93 | addItemExtension("itunes:title", ItunesItem::setItunesTitle);
94 | addItemExtension("itunes:subtitle", ItunesItem::setItunesSubtitle);
95 | addItemExtension("itunes:summary", ItunesItem::setItunesSummary);
96 | addItemExtension("itunes:keywords", ItunesItem::setItunesKeywords);
97 | addItemExtension("itunes:episode", (i, v) -> mapInteger(v, i::setItunesEpisode));
98 | addItemExtension("itunes:season", (i, v) -> mapInteger(v, i::setItunesSeason));
99 | addItemExtension("itunes:episodeType", ItunesItem::setItunesEpisodeType);
100 | addItemExtension("itunes:block", (i, v) -> mapBoolean(v, i::setItunesBlock));
101 | addItemExtension("itunes:image", "href", ItunesItem::setItunesImage);
102 | }
103 |
104 | @Override
105 | protected ItunesChannel createChannel(DateTimeParser dateTimeParser) {
106 | return new ItunesChannel(dateTimeParser);
107 | }
108 |
109 | @Override
110 | protected ItunesItem createItem(DateTimeParser dateTimeParser) {
111 | return new ItunesItem(dateTimeParser);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/module/mediarss/MediaRssItem.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2022, Apptastic Software
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.apptasticsoftware.rssreader.module.mediarss;
25 |
26 | import com.apptasticsoftware.rssreader.DateTimeParser;
27 | import com.apptasticsoftware.rssreader.Item;
28 |
29 | import java.util.Objects;
30 | import java.util.Optional;
31 |
32 | /**
33 | * Class representing the media rss item.
34 | */
35 | public class MediaRssItem extends Item {
36 | private MediaThumbnail mediaThumbnail;
37 |
38 | /**
39 | * Constructor
40 | *
41 | * @param dateTimeParser timestamp parser
42 | */
43 | public MediaRssItem(DateTimeParser dateTimeParser) {
44 | super(dateTimeParser);
45 | }
46 |
47 | /**
48 | * Get the media thumbnail
49 | *
50 | * @return media thumbnail
51 | */
52 | public Optional getMediaThumbnail() {
53 | return Optional.ofNullable(mediaThumbnail);
54 | }
55 |
56 | /**
57 | * Set the media thumbnail
58 | *
59 | * @param mediaThumbnail media thumbnail
60 | */
61 | public void setMediaThumbnail(MediaThumbnail mediaThumbnail) {
62 | this.mediaThumbnail = mediaThumbnail;
63 | }
64 |
65 | @Override
66 | public boolean equals(Object o) {
67 | if (this == o) return true;
68 | if (o == null || getClass() != o.getClass()) return false;
69 | if (!super.equals(o)) return false;
70 | MediaRssItem that = (MediaRssItem) o;
71 | return Objects.equals(getMediaThumbnail(), that.getMediaThumbnail());
72 | }
73 |
74 | @Override
75 | public int hashCode() {
76 | return Objects.hash(super.hashCode(), getMediaThumbnail());
77 | }
78 | }
79 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/module/mediarss/MediaRssReader.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2022, Apptastic Software
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.apptasticsoftware.rssreader.module.mediarss;
25 |
26 | import com.apptasticsoftware.rssreader.AbstractRssReader;
27 | import com.apptasticsoftware.rssreader.Channel;
28 | import com.apptasticsoftware.rssreader.DateTimeParser;
29 |
30 | import java.net.http.HttpClient;
31 | import java.util.function.BiConsumer;
32 |
33 | /**
34 | * Class for reading media rss feeds.
35 | */
36 | public class MediaRssReader extends AbstractRssReader {
37 |
38 | /**
39 | * Constructor
40 | */
41 | public MediaRssReader() {
42 | super();
43 | }
44 |
45 | /**
46 | * Constructor
47 | * @param httpClient http client
48 | */
49 | public MediaRssReader(HttpClient httpClient) {
50 | super(httpClient);
51 | }
52 |
53 | @Override
54 | protected Channel createChannel(DateTimeParser dateTimeParser) {
55 | return new Channel(dateTimeParser);
56 | }
57 |
58 | @Override
59 | protected MediaRssItem createItem(DateTimeParser dateTimeParser) {
60 | return new MediaRssItem(dateTimeParser);
61 | }
62 |
63 | @SuppressWarnings("java:S1192")
64 | @Override
65 | protected void registerItemAttributes() {
66 | super.registerItemAttributes();
67 | super.addItemExtension("media:thumbnail", "url", mediaThumbnailSetterTemplateBuilder(MediaThumbnail::setUrl));
68 | super.addItemExtension("media:thumbnail", "height", mediaThumbnailSetterTemplateBuilder(
69 | (mediaThumbnail, height) -> mediaThumbnail.setHeight(Integer.parseInt(height))
70 | ));
71 | super.addItemExtension("media:thumbnail", "width", mediaThumbnailSetterTemplateBuilder(
72 | (mediaThumbnail, width) -> mediaThumbnail.setWidth(Integer.parseInt(width))
73 | ));
74 | }
75 |
76 | private BiConsumer mediaThumbnailSetterTemplateBuilder(BiConsumer setter) {
77 | return (mediaRssItem, value) -> {
78 | var mediaThumbnail = mediaRssItem.getMediaThumbnail().orElse(new MediaThumbnail());
79 | setter.accept(mediaThumbnail, value);
80 | mediaRssItem.setMediaThumbnail(mediaThumbnail);
81 | };
82 | }
83 | }
84 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/module/mediarss/MediaThumbnail.java:
--------------------------------------------------------------------------------
1 | package com.apptasticsoftware.rssreader.module.mediarss;
2 |
3 | import java.util.Optional;
4 |
5 | /**
6 | * Class representing the media thumbnail from the media rss spec.
7 | * See for details.
8 | */
9 | public class MediaThumbnail {
10 | private String url;
11 | private Integer width;
12 | private Integer height;
13 | private String time;
14 |
15 | /**
16 | * Get the url of the thumbnail
17 | *
18 | * @return url
19 | */
20 | public String getUrl() {
21 | return url;
22 | }
23 |
24 | /**
25 | * Set the url of the thumbnail
26 | *
27 | * @param url url
28 | */
29 | public void setUrl(String url) {
30 | this.url = url;
31 | }
32 |
33 | /**
34 | * Get the width of the thumbnail
35 | *
36 | * @return width
37 | */
38 | public Optional getWidth() {
39 | return Optional.ofNullable(width);
40 | }
41 |
42 | /**
43 | * Set the width of the thumbnail
44 | *
45 | * @param width width
46 | */
47 | public void setWidth(Integer width) {
48 | this.width = width;
49 | }
50 |
51 | /**
52 | * Get the height of the thumbnail
53 | *
54 | * @return height
55 | */
56 | public Optional getHeight() {
57 | return Optional.ofNullable(height);
58 | }
59 |
60 | /**
61 | * Set the height of the thumbnail
62 | *
63 | * @param height height
64 | */
65 | public void setHeight(Integer height) {
66 | this.height = height;
67 | }
68 |
69 | /**
70 | * Get the time of the thumbnail
71 | *
72 | * @return time
73 | */
74 | public Optional getTime() {
75 | return Optional.ofNullable(time);
76 | }
77 |
78 | /**
79 | * Set the time of the thumbnail
80 | *
81 | * @param time time
82 | */
83 | public void setTime(String time) {
84 | this.time = time;
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/package-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2022, Apptastic Software
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | /**
26 | * This package is intended for RSS reader.
27 | */
28 | package com.apptasticsoftware.rssreader;
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/util/Default.java:
--------------------------------------------------------------------------------
1 | package com.apptasticsoftware.rssreader.util;
2 |
3 | import com.apptasticsoftware.rssreader.DateTime;
4 | import com.apptasticsoftware.rssreader.DateTimeParser;
5 |
6 | /**
7 | * Provides default implementations for various components.
8 | */
9 | @SuppressWarnings("javaarchitecture:S7091")
10 | public class Default {
11 |
12 | private Default() {
13 | // Utility class
14 | }
15 |
16 | /**
17 | * Get the default date time parser.
18 | * @return date time parser
19 | */
20 | public static DateTimeParser getDateTimeParser() {
21 | return new DateTime();
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/util/ItemComparator.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2022, Apptastic Software
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 | package com.apptasticsoftware.rssreader.util;
25 |
26 | import com.apptasticsoftware.rssreader.Channel;
27 | import com.apptasticsoftware.rssreader.DateTimeParser;
28 | import com.apptasticsoftware.rssreader.Item;
29 |
30 | import java.util.Comparator;
31 | import java.util.Objects;
32 |
33 | /**
34 | * Provides different comparators for sorting item objects.
35 | */
36 | @SuppressWarnings("java:S1133")
37 | public final class ItemComparator {
38 | private static final String MUST_NOT_BE_NULL_MESSAGE = "Date time parser must not be null";
39 |
40 | private ItemComparator() {
41 |
42 | }
43 |
44 | /**
45 | * Comparator for sorting Items on initial creation or first availability (publication date) in ascending order (oldest first)
46 | * @param any class that extends Item
47 | * @return comparator
48 | *
49 | * @deprecated As of release 3.9.0, replaced by {@link #oldestPublishedItemFirst()}
50 | */
51 | @Deprecated(since = "3.9.0", forRemoval = true)
52 | public static Comparator oldestItemFirst() {
53 | return oldestPublishedItemFirst();
54 | }
55 |
56 | /**
57 | * Comparator for sorting Items on initial creation or first availability (publication date) in ascending order (oldest first)
58 | * @param any class that extends Item
59 | * @return comparator
60 | */
61 | public static Comparator oldestPublishedItemFirst() {
62 | return Comparator.comparing((I i) ->
63 | i.getPubDateZonedDateTime().orElse(null),
64 | Comparator.nullsLast(Comparator.naturalOrder()));
65 | }
66 |
67 | /**
68 | * Comparator for sorting Items on updated date if exist otherwise on publication date in ascending order (oldest first)
69 | * @param any class that extends Item
70 | * @return comparator
71 | */
72 | public static Comparator oldestUpdatedItemFirst() {
73 | return Comparator.comparing((I i) ->
74 | i.getUpdatedZonedDateTime().orElse(i.getPubDateZonedDateTime().orElse(null)),
75 | Comparator.nullsLast(Comparator.naturalOrder()));
76 | }
77 |
78 | /**
79 | * Comparator for sorting Items on initial creation or first availability (publication date) in ascending order (oldest first)
80 | * @param any class that extends Item
81 | * @param dateTimeParser date time parser
82 | * @return comparator
83 | *
84 | * @deprecated As of release 3.9.0, replaced by {@link #oldestPublishedItemFirst(DateTimeParser)}
85 | */
86 | @Deprecated(since = "3.9.0", forRemoval = true)
87 | public static Comparator oldestItemFirst(DateTimeParser dateTimeParser) {
88 | return oldestPublishedItemFirst(dateTimeParser);
89 | }
90 |
91 | /**
92 | * Comparator for sorting Items on initial creation or first availability (publication date) in ascending order (oldest first)
93 | * @param any class that extends Item
94 | * @param dateTimeParser date time parser
95 | * @return comparator
96 | */
97 | public static Comparator oldestPublishedItemFirst(DateTimeParser dateTimeParser) {
98 | Objects.requireNonNull(dateTimeParser, MUST_NOT_BE_NULL_MESSAGE);
99 | return Comparator.comparing((I i) ->
100 | i.getPubDate().map(dateTimeParser::parse).orElse(null),
101 | Comparator.nullsLast(Comparator.naturalOrder()));
102 | }
103 |
104 | /**
105 | * Comparator for sorting Items on updated date if exist otherwise on publication date in ascending order (oldest first)
106 | * @param any class that extends Item
107 | * @param dateTimeParser date time parser
108 | * @return comparator
109 | */
110 | public static Comparator oldestUpdatedItemFirst(DateTimeParser dateTimeParser) {
111 | Objects.requireNonNull(dateTimeParser, MUST_NOT_BE_NULL_MESSAGE);
112 | return Comparator.comparing((I i) ->
113 | i.getUpdated().or(i::getPubDate).map(dateTimeParser::parse).orElse(null),
114 | Comparator.nullsLast(Comparator.naturalOrder()));
115 | }
116 |
117 | /**
118 | * Comparator for sorting Items on initial creation or first availability (publication date) in descending order (newest first)
119 | * @param any class that extends Item
120 | * @return comparator
121 | *
122 | * @deprecated As of release 3.9.0, replaced by {@link #newestPublishedItemFirst()}
123 | */
124 | @Deprecated(since = "3.9.0", forRemoval = true)
125 | public static Comparator newestItemFirst() {
126 | return newestPublishedItemFirst();
127 | }
128 |
129 | /**
130 | * Comparator for sorting Items on initial creation or first availability (publication date) in descending order (newest first)
131 | * @param any class that extends Item
132 | * @return comparator
133 | */
134 | public static Comparator newestPublishedItemFirst() {
135 | return Comparator.comparing((I i) ->
136 | i.getPubDateZonedDateTime().orElse(null),
137 | Comparator.nullsLast(Comparator.naturalOrder())).reversed();
138 | }
139 |
140 | /**
141 | * Comparator for sorting Items on updated date if exist otherwise on publication date in descending order (newest first)
142 | * @param any class that extends Item
143 | * @return comparator
144 | */
145 | public static Comparator newestUpdatedItemFirst() {
146 | return Comparator.comparing((I i) ->
147 | i.getUpdatedZonedDateTime().orElse(i.getPubDateZonedDateTime().orElse(null)),
148 | Comparator.nullsLast(Comparator.naturalOrder())).reversed();
149 | }
150 |
151 | /**
152 | * Comparator for sorting Items on initial creation or first availability (publication date) in descending order (newest first)
153 | * @param any class that extends Item
154 | * @param dateTimeParser date time parser
155 | * @return comparator
156 | *
157 | * @deprecated As of release 3.9.0, replaced by {@link #newestPublishedItemFirst(DateTimeParser)}
158 | */
159 | @Deprecated(since = "3.9.0", forRemoval = true)
160 | public static Comparator newestItemFirst(DateTimeParser dateTimeParser) {
161 | return newestPublishedItemFirst(dateTimeParser);
162 | }
163 |
164 | /**
165 | * Comparator for sorting Items on initial creation or first availability (publication date) in descending order (newest first)
166 | * @param any class that extends Item
167 | * @param dateTimeParser date time parser
168 | * @return comparator
169 | */
170 | public static Comparator newestPublishedItemFirst(DateTimeParser dateTimeParser) {
171 | Objects.requireNonNull(dateTimeParser, MUST_NOT_BE_NULL_MESSAGE);
172 | return Comparator.comparing((I i) ->
173 | i.getPubDate().map(dateTimeParser::parse).orElse(null),
174 | Comparator.nullsLast(Comparator.naturalOrder())).reversed();
175 | }
176 |
177 | /**
178 | * Comparator for sorting Items on updated date if exist otherwise on publication date in descending order (newest first)
179 | * @param any class that extends Item
180 | * @param dateTimeParser date time parser
181 | * @return comparator
182 | */
183 | public static Comparator newestUpdatedItemFirst(DateTimeParser dateTimeParser) {
184 | Objects.requireNonNull(dateTimeParser, MUST_NOT_BE_NULL_MESSAGE);
185 | return Comparator.comparing((I i) ->
186 | i.getUpdated().or(i::getPubDate).map(dateTimeParser::parse).orElse(null),
187 | Comparator.nullsLast(Comparator.naturalOrder())).reversed();
188 | }
189 |
190 | /**
191 | * Comparator for sorting Items on channel title
192 | * @param any class that extends Item
193 | * @return comparator
194 | */
195 | public static Comparator channelTitle() {
196 | return Comparator.comparing(
197 | Item::getChannel,
198 | Comparator.nullsFirst(Comparator.comparing(
199 | Channel::getTitle, Comparator.nullsFirst(Comparator.naturalOrder()))));
200 | }
201 |
202 | }
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/util/Mapper.java:
--------------------------------------------------------------------------------
1 | package com.apptasticsoftware.rssreader.util;
2 |
3 | import java.util.Optional;
4 | import java.util.function.Consumer;
5 | import java.util.function.Function;
6 | import java.util.function.Supplier;
7 | import java.util.logging.Level;
8 | import java.util.logging.Logger;
9 |
10 | /**
11 | * Provides methods for mapping field
12 | */
13 | public final class Mapper {
14 | private static final Logger LOGGER = Logger.getLogger("com.apptasticsoftware.rssreader.util");
15 |
16 | private Mapper() { }
17 |
18 | /**
19 | * Maps a boolean text value (true, false, no or yes) to a boolean field. Text value can be in any casing.
20 | * @param text text value
21 | * @param func boolean setter method
22 | */
23 | public static void mapBoolean(String text, Consumer func) {
24 | text = text.toLowerCase();
25 | if ("true".equals(text) || "yes".equals(text)) {
26 | func.accept(Boolean.TRUE);
27 | } else if ("false".equals(text) || "no".equals(text)) {
28 | func.accept(Boolean.FALSE);
29 | }
30 | }
31 |
32 | /**
33 | * Maps a integer text value to a integer field.
34 | * @param text text value
35 | * @param func integer setter method
36 | */
37 | public static void mapInteger(String text, Consumer func) {
38 | mapNumber(text, func, Integer::valueOf);
39 | }
40 |
41 | /**
42 | * Maps a long text value to a long field.
43 | * @param text text value
44 | * @param func long setter method
45 | */
46 | public static void mapLong(String text, Consumer func) {
47 | mapNumber(text, func, Long::valueOf);
48 | }
49 |
50 | private static void mapNumber(String text, Consumer func, Function convert) {
51 | if (!isNullOrEmpty(text)) {
52 | try {
53 | func.accept(convert.apply(text));
54 | } catch (NumberFormatException e) {
55 | if (LOGGER.isLoggable(Level.WARNING)) {
56 | LOGGER.log(Level.WARNING, () -> String.format("Failed to convert %s. Message: %s", text, e.getMessage()));
57 | }
58 | }
59 | }
60 | }
61 |
62 | /**
63 | * Map value if field has not been mapped before
64 | * @param text value to map
65 | * @param getter getter to check if field is empty
66 | * @param setter setter to set value
67 | * @param type
68 | */
69 | public static void mapIfEmpty(String text, Supplier getter, Consumer setter) {
70 | if (isNullOrEmpty(getter) && !isNullOrEmpty(text)) {
71 | setter.accept(text);
72 | }
73 | }
74 |
75 | /**
76 | * Create a new instance if a getter returns optional empty and assigns the field the new instance.
77 | * @param getter getter method
78 | * @param setter setter method
79 | * @param factory factory for creating a new instance if field is not set before
80 | * @return existing or new instance
81 | * @param any class
82 | */
83 | public static T createIfNull(Supplier> getter, Consumer setter, Supplier factory) {
84 | return createIfNullOptional(getter, setter, factory).orElse(null);
85 | }
86 |
87 | /**
88 | * Create a new instance if a getter returns optional empty and assigns the field the new instance.
89 | * @param getter getter method
90 | * @param setter setter method
91 | * @param factory factory for creating a new instance if field is not set before
92 | * @return existing or new instance
93 | * @param any class
94 | */
95 | public static Optional createIfNullOptional(Supplier> getter, Consumer setter, Supplier factory) {
96 | Optional instance = getter.get();
97 | if (instance.isEmpty()) {
98 | T newInstance = factory.get();
99 | setter.accept(newInstance);
100 | instance = Optional.of(newInstance);
101 | }
102 | return instance;
103 | }
104 |
105 | private static boolean isNullOrEmpty(Supplier getter) {
106 | return getter.get() == null ||
107 | "".equals(getter.get()) ||
108 | getter.get() == Optional.empty() ||
109 | getter.get() instanceof Optional> &&
110 | ((Optional>) getter.get())
111 | .filter(String.class::isInstance)
112 | .map(String.class::cast)
113 | .map(String::isBlank)
114 | .orElse(false);
115 | }
116 |
117 | private static boolean isNullOrEmpty(String text) {
118 | return text == null || text.isBlank();
119 | }
120 | }
121 |
--------------------------------------------------------------------------------
/src/main/java/com/apptasticsoftware/rssreader/util/Util.java:
--------------------------------------------------------------------------------
1 | package com.apptasticsoftware.rssreader.util;
2 |
3 | import java.util.Locale;
4 |
5 | /**
6 | * Utility class for RSS reader.
7 | */
8 | public class Util {
9 |
10 | private Util() {
11 |
12 | }
13 |
14 | /**
15 | * Convert a time period string to hours.
16 | *
17 | * @param period the time period string (e.g., "daily", "weekly", "monthly", "yearly", "hourly")
18 | * @return the number of hours in the given time period, or 1 if the period is not recognized
19 | */
20 | public static int toMinutes(String period) {
21 | switch (period.toLowerCase(Locale.ENGLISH)) {
22 | case "daily": return 1440;
23 | case "weekly": return 10080;
24 | case "monthly": return 43800;
25 | case "yearly": return 525600;
26 | case "hourly":
27 | default: return 60;
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/main/java/module-info.java:
--------------------------------------------------------------------------------
1 | /*
2 | * MIT License
3 | *
4 | * Copyright (c) 2022, Apptastic Software
5 | *
6 | * Permission is hereby granted, free of charge, to any person obtaining a copy
7 | * of this software and associated documentation files (the "Software"), to deal
8 | * in the Software without restriction, including without limitation the rights
9 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10 | * copies of the Software, and to permit persons to whom the Software is
11 | * furnished to do so, subject to the following conditions:
12 | *
13 | * The above copyright notice and this permission notice shall be included in all
14 | * copies or substantial portions of the Software.
15 | *
16 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 | * SOFTWARE.
23 | */
24 |
25 | /**
26 | * These modules define the base APIs for RSS reader.
27 | */
28 | module com.apptasticsoftware.rssreader {
29 | requires java.net.http;
30 | requires java.xml;
31 | requires java.logging;
32 |
33 | exports com.apptasticsoftware.rssreader;
34 | exports com.apptasticsoftware.rssreader.util;
35 | exports com.apptasticsoftware.rssreader.module.itunes;
36 | exports com.apptasticsoftware.rssreader.module.mediarss;
37 | }
--------------------------------------------------------------------------------
/src/test/java/com/apptasticsoftware/integrationtest/ConnectionTest.java:
--------------------------------------------------------------------------------
1 | package com.apptasticsoftware.integrationtest;
2 |
3 | import com.apptasticsoftware.rssreader.Item;
4 | import com.apptasticsoftware.rssreader.RssReader;
5 | import com.apptasticsoftware.rssreader.internal.RssServer;
6 | import org.junit.jupiter.api.Test;
7 |
8 | import java.io.File;
9 | import java.io.IOException;
10 | import java.time.Duration;
11 | import java.time.ZonedDateTime;
12 | import java.util.List;
13 | import java.util.stream.Collectors;
14 |
15 | import static org.junit.jupiter.api.Assertions.*;
16 |
17 | class ConnectionTest {
18 | private static final int PORT = 8008;
19 | private static final Duration NEGATIVE_DURATION = Duration.ofSeconds(-30);
20 |
21 | @Test
22 | void testConnectionTimeoutWithNullValue() {
23 | var rssReader = new RssReader();
24 | var exception = assertThrows(NullPointerException.class, () -> rssReader.setConnectionTimeout(null));
25 | assertEquals("Connection timeout must not be null", exception.getMessage());
26 | }
27 |
28 | @Test
29 | void testRequestTimeoutWithNullValue() {
30 | var rssReader = new RssReader();
31 | var exception = assertThrows(NullPointerException.class, () -> rssReader.setRequestTimeout(null));
32 | assertEquals("Request timeout must not be null", exception.getMessage());
33 | }
34 |
35 | @Test
36 | void testReadTimeoutWithNullValue() {
37 | var rssReader = new RssReader();
38 | var exception = assertThrows(NullPointerException.class, () -> rssReader.setReadTimeout(null));
39 | assertEquals("Read timeout must not be null", exception.getMessage());
40 | }
41 |
42 | @Test
43 | void testConnectionTimeoutWithNegativeValue() {
44 | var rssReader = new RssReader();
45 | var exception = assertThrows(IllegalArgumentException.class, () -> rssReader.setConnectionTimeout(NEGATIVE_DURATION));
46 | assertEquals("Connection timeout must not be negative", exception.getMessage());
47 | }
48 |
49 | @Test
50 | void testRequestTimeoutWithNegativeValue() {
51 | var rssReader = new RssReader();
52 | var exception = assertThrows(IllegalArgumentException.class, () -> rssReader.setRequestTimeout(NEGATIVE_DURATION));
53 | assertEquals("Request timeout must not be negative", exception.getMessage());
54 | }
55 |
56 | @Test
57 | void testReadTimeoutWithNegativeValue() {
58 | var rssReader = new RssReader();
59 | var exception = assertThrows(IllegalArgumentException.class, () -> rssReader.setReadTimeout(NEGATIVE_DURATION));
60 | assertEquals("Read timeout must not be negative", exception.getMessage());
61 | }
62 |
63 | @Test
64 | void testReadFromLocalRssServerNoTimeout() throws IOException {
65 | var server = RssServer.with(getFile("atom-feed.xml"))
66 | .port(PORT)
67 | .endpointPath("/rss")
68 | .build();
69 | server.start();
70 |
71 | var items = new RssReader()
72 | .setConnectionTimeout(Duration.ZERO)
73 | .setRequestTimeout(Duration.ZERO)
74 | .setReadTimeout(Duration.ZERO)
75 | .read("http://localhost:8008/rss")
76 | .collect(Collectors.toList());
77 |
78 | server.stop();
79 | verify(3, items);
80 | }
81 |
82 | @Test
83 | void testReadFromLocalRssServer10SecondTimeout() throws IOException {
84 | var server = RssServer.with(getFile("atom-feed.xml"))
85 | .port(PORT)
86 | .endpointPath("/rss")
87 | .build();
88 | server.start();
89 |
90 | var items = new RssReader()
91 | .setConnectionTimeout(Duration.ofSeconds(10))
92 | .setRequestTimeout(Duration.ofSeconds(10))
93 | .setReadTimeout(Duration.ofSeconds(10))
94 | .read("http://localhost:8008/rss")
95 | .collect(Collectors.toList());
96 |
97 | server.stop();
98 | verify(3, items);
99 | }
100 |
101 |
102 | @Test
103 | void testReadFromLocalRssServer() throws IOException {
104 | var server = RssServer.with(getFile("atom-feed.xml"))
105 | .port(PORT)
106 | .endpointPath("/rss")
107 | .build();
108 | server.start();
109 |
110 | var items = new RssReader()
111 | .setReadTimeout(Duration.ofSeconds(2))
112 | .read("http://localhost:8008/rss")
113 | .collect(Collectors.toList());
114 |
115 | server.stop();
116 | verify(3, items);
117 | }
118 |
119 | @Test
120 | void testNoReadTimeout() throws IOException {
121 | var server = RssServer.with(getFile("atom-feed.xml"))
122 | .port(PORT)
123 | .endpointPath("/rss")
124 | .build();
125 | server.start();
126 |
127 | var items = new RssReader()
128 | .setReadTimeout(Duration.ZERO)
129 | .read("http://localhost:8008/rss")
130 | .collect(Collectors.toList());
131 |
132 | server.stop();
133 | verify(3, items);
134 | }
135 |
136 | @Test
137 | void testReadTimeout() throws IOException {
138 | var server = RssServer.withWritePause(getFile("atom-feed.xml"), Duration.ofSeconds(4))
139 | .port(PORT)
140 | .endpointPath("/slow-server")
141 | .build();
142 | server.start();
143 |
144 | var items = new RssReader()
145 | .setReadTimeout(Duration.ofSeconds(2))
146 | .read("http://localhost:8008/slow-server")
147 | .collect(Collectors.toList());
148 |
149 | server.stop();
150 | verify(2, items);
151 | }
152 |
153 | private static void verify(int expectedSize, List items) {
154 | assertEquals(expectedSize, items.size());
155 |
156 | if (!items.isEmpty()) {
157 | assertEquals("dive into mark", items.get(0).getChannel().getTitle());
158 | assertEquals(65, items.get(0).getChannel().getDescription().length());
159 | assertEquals("http://example.org/feed.atom", items.get(0).getChannel().getLink());
160 | assertEquals("Copyright (c) 2003, Mark Pilgrim", items.get(0).getChannel().getCopyright().orElse(null));
161 | assertEquals("Example Toolkit", items.get(0).getChannel().getGenerator().orElse(null));
162 | assertEquals("2005-07-31T12:29:29Z", items.get(0).getChannel().getLastBuildDate().orElse(null));
163 |
164 | assertEquals("Atom draft-07 snapshot", items.get(0).getTitle().orElse(null));
165 | assertNull(items.get(1).getAuthor().orElse(null));
166 | assertEquals("http://example.org/audio/ph34r_my_podcast.mp3", items.get(0).getLink().orElse(null));
167 | assertEquals("tag:example.org,2003:3.2397", items.get(0).getGuid().orElse(null));
168 | assertEquals("2003-12-13T08:29:29-04:00", items.get(0).getPubDate().orElse(null));
169 | assertEquals("2005-07-31T12:29:29Z", items.get(0).getUpdated().orElse(null));
170 | assertEquals(211, items.get(1).getDescription().orElse("").length());
171 | }
172 | if (items.size() >= 2) {
173 | assertEquals("Atom-Powered Robots Run Amok", items.get(1).getTitle().orElse(null));
174 | assertNull(items.get(1).getAuthor().orElse(null));
175 | assertEquals("http://example.org/2003/12/13/atom03", items.get(1).getLink().orElse(null));
176 | assertEquals("urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a", items.get(1).getGuid().orElse(null));
177 | assertEquals("2003-12-13T18:30:02Z", items.get(1).getPubDate().orElse(null));
178 | assertEquals("2003-12-13T18:30:02Z", items.get(1).getUpdated().orElse(null));
179 | assertEquals(211, items.get(1).getDescription().orElse("").length());
180 | }
181 | if (items.size() >= 3) {
182 | assertEquals("Atom-Powered Robots Run Amok 2", items.get(2).getTitle().orElse(null));
183 | assertNull(items.get(2).getAuthor().orElse(null));
184 | assertEquals("http://example.org/2003/12/13/atom04", items.get(2).getLink().orElse(null));
185 | assertEquals("urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6b", items.get(2).getGuid().orElse(null));
186 | assertEquals("2003-12-13T09:28:28-04:00", items.get(2).getPubDate().orElse(null));
187 | assertEquals(1071322108, items.get(2).getPubDateZonedDateTime().map(ZonedDateTime::toEpochSecond).orElse(null));
188 | assertEquals("2003-12-13T18:30:01Z", items.get(2).getUpdated().orElse(null));
189 | assertEquals(1071340201, items.get(2).getUpdatedZonedDateTime().map(ZonedDateTime::toEpochSecond).orElse(null));
190 | assertEquals(47, items.get(2).getDescription().orElse("").length());
191 | }
192 | }
193 |
194 | private File getFile(String filename) {
195 | var url = getClass().getClassLoader().getResource(filename);
196 | return new File(url.getFile());
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/src/test/java/com/apptasticsoftware/integrationtest/SortTest.java:
--------------------------------------------------------------------------------
1 | package com.apptasticsoftware.integrationtest;
2 |
3 | import com.apptasticsoftware.rssreader.Item;
4 | import com.apptasticsoftware.rssreader.RssReader;
5 | import com.apptasticsoftware.rssreader.util.ItemComparator;
6 | import org.junit.jupiter.api.Test;
7 |
8 | import java.io.IOException;
9 | import java.util.List;
10 | import java.util.Optional;
11 | import java.util.stream.Collectors;
12 |
13 | import static org.junit.jupiter.api.Assertions.assertNotEquals;
14 | import static org.junit.jupiter.api.Assertions.assertTrue;
15 | import static org.junit.jupiter.api.Assertions.assertFalse;
16 |
17 | class SortTest {
18 |
19 | @Test
20 | void testTimestampSortTest() {
21 | var urlList = List.of(
22 | "https://www.riksbank.se/sv/rss/pressmeddelanden",
23 | "https://www.konj.se/4.2de5c57614f808a95afcc13f/12.2de5c57614f808a95afcc354.portlet?state=rss&sv.contenttype=text/xml;charset=UTF-8",
24 | "https://www.scb.se/Feed/statistiknyheter/",
25 | "https://www.avanza.se/placera/forstasidan.rss.xml",
26 | "https://www.breakit.se/feed/artiklar",
27 | "https://feedforall.com/sample-feed.xml",
28 | "https://se.investing.com/rss/news.rss",
29 | "https://www.di.se/digital/rss",
30 | "https://worldoftanks.eu/en/rss/news/",
31 | "https://rss.nytimes.com/services/xml/rss/nyt/HomePage.xml",
32 | "https://github.com/openjdk/jdk/commits.atom",
33 | "https://www.microsoft.com/releasecommunications/api/v2/azure/rss",
34 | "https://blog.ploeh.dk/rss.xml",
35 | "https://www.politico.com/rss/politicopicks.xml",
36 | "https://www.e1.ru/talk/forum/rss.php?f=86",
37 | "https://failed-to-read-from-this-url.com",
38 | "https://www.nrdc.org/rss.xml",
39 | "https://www.theverge.com/rss/reviews/index.xml",
40 | "https://feeds.macrumors.com/MacRumors-All",
41 | "https://www.ksl.com/rss/news",
42 | "http://rss.cnn.com/rss/cnn_latest.rss",
43 | "https://moxie.foxnews.com/google-publisher/latest.xml",
44 | "https://techcrunch.com/feed/",
45 | "https://feeds.arstechnica.com/arstechnica/science"
46 | );
47 |
48 | var timestamps = new RssReader().read(urlList)
49 | .sorted()
50 | .map(Item::getPubDateZonedDateTime)
51 | .flatMap(Optional::stream)
52 | .map(t -> t.toInstant().toEpochMilli())
53 | .collect(Collectors.toList());
54 |
55 | assertTrue(timestamps.size() > 200);
56 |
57 | var iterator = timestamps.iterator();
58 | Long current, previous = iterator.next();
59 | while (iterator.hasNext()) {
60 | current = iterator.next();
61 | assertTrue(previous.compareTo(current) >= 0);
62 | previous = current;
63 | }
64 | }
65 |
66 | @Test
67 | void testSortNewestFirst() throws IOException {
68 | var list = new RssReader().read("https://feeds.macrumors.com/MacRumors-All")
69 | .sorted(ItemComparator.newestPublishedItemFirst())
70 | .collect(Collectors.toList());
71 |
72 | assertFalse(list.isEmpty());
73 |
74 | var previous = list.get(0);
75 | for (Item current : list) {
76 | assertTrue(previous.compareTo(current) <= 0);
77 | previous = current;
78 | }
79 | }
80 |
81 | @Test
82 | void testSortOldestFirst() throws IOException {
83 | var list = new RssReader().read("https://feeds.macrumors.com/MacRumors-All")
84 | .sorted(ItemComparator.oldestPublishedItemFirst())
85 | .collect(Collectors.toList());
86 |
87 | assertFalse(list.isEmpty());
88 |
89 | var previous = list.get(0);
90 | for (Item current : list) {
91 | assertTrue(previous.compareTo(current) >= 0);
92 | previous = current;
93 | }
94 | }
95 |
96 | @Test
97 | void testSortChannelTitle() {
98 | var urls = List.of("https://feeds.a.dj.com/rss/RSSMarketsMain.xml", "https://gizmodo.com/feed");
99 | var list = new RssReader().read(urls)
100 | .sorted(ItemComparator.channelTitle())
101 | .collect(Collectors.toList());
102 |
103 | var first = list.get(0);
104 | var last = list.get(list.size() - 1);
105 | assertNotEquals(first.getChannel().getTitle(), last.getChannel().getTitle());
106 | assertTrue(first.getChannel().getTitle().toLowerCase().contains("gizmodo"));
107 | assertTrue(last.getChannel().getTitle().toLowerCase().contains("wsj"));
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/src/test/java/com/apptasticsoftware/rssreader/internal/RssServer.java:
--------------------------------------------------------------------------------
1 | package com.apptasticsoftware.rssreader.internal;
2 |
3 | import com.sun.net.httpserver.HttpExchange;
4 | import com.sun.net.httpserver.HttpHandler;
5 | import com.sun.net.httpserver.HttpServer;
6 |
7 | import java.io.*;
8 | import java.net.InetSocketAddress;
9 | import java.nio.file.Files;
10 | import java.time.Duration;
11 | import java.time.Instant;
12 | import java.util.Objects;
13 | import java.util.concurrent.TimeUnit;
14 | import java.util.logging.Logger;
15 |
16 | /**
17 | * Basic RSS server from testing
18 | */
19 | public class RssServer {
20 | private static final Logger LOGGER = Logger.getLogger("RssServer");
21 | private final HttpServer server;
22 |
23 | private RssServer(int port, String endpointPath, File file, Duration writeBodyPause) throws IOException {
24 | server = HttpServer.create(new InetSocketAddress(port), 0);
25 | server.createContext(endpointPath, new FileRssHandler(file, writeBodyPause));
26 | server.setExecutor(null);
27 | }
28 |
29 | /**
30 | * RSS server that publish the given file content as an RSS/Atom feed.
31 | * @param file content to publish
32 | * @return RSS server
33 | */
34 | public static RssServerBuilder with(File file) {
35 | Objects.requireNonNull(file, "File must not be null");
36 | if (!file.isFile()) {
37 | throw new IllegalArgumentException("File must exist");
38 | }
39 | return new RssServerBuilder(file, Duration.ZERO);
40 | }
41 |
42 | /**
43 | * RSS server that publish the given file content as an RSS/Atom feed.
44 | * Server will publish 90% of the data and then wait the given amount of time before publish the rest of the data.
45 | * @param file content to publish
46 | * @param writeBodyPause time to wait before publishing the last data
47 | * @return RSS server
48 | */
49 | public static RssServerBuilder withWritePause(File file, Duration writeBodyPause) {
50 | Objects.requireNonNull(file, "File must not be null");
51 | if (!file.isFile()) {
52 | throw new IllegalArgumentException("File must exist");
53 | }
54 | Objects.requireNonNull(writeBodyPause, "Write body pause must not be null");
55 | if (writeBodyPause.isNegative()) {
56 | throw new IllegalArgumentException("Write body pause must not be negative");
57 | }
58 | return new RssServerBuilder(file, writeBodyPause);
59 | }
60 |
61 | /**
62 | * Start RSS server
63 | */
64 | public void start() {
65 | server.start();
66 | }
67 |
68 | /**
69 | * Stop RSS server
70 | */
71 | public void stop() {
72 | server.stop(1);
73 | }
74 |
75 | private static class FileRssHandler implements HttpHandler {
76 | private final File file;
77 | private final Duration writeBodyPause;
78 |
79 | public FileRssHandler(File file, Duration writeBodyPause) {
80 | this.file = file;
81 | this.writeBodyPause = writeBodyPause;
82 | }
83 |
84 | @Override
85 | public void handle(HttpExchange exchange) throws IOException {
86 | LOGGER.info("New connection " + Instant.now());
87 | var responseBodyLength = Files.size(file.toPath());
88 | exchange.sendResponseHeaders(200, responseBodyLength);
89 |
90 | try (var os = exchange.getResponseBody()) {
91 | writeResponseBody(os, responseBodyLength);
92 | }
93 |
94 | LOGGER.info("Connection closed " + Instant.now());
95 | }
96 |
97 | private void writeResponseBody(OutputStream os, long responseBodyLength) throws IOException {
98 | byte[] buffer = new byte[128];
99 | int readLength;
100 | int totalReadLength = 0;
101 | boolean hasPaused = false;
102 |
103 | try (var is = new FileInputStream(file)){
104 | while ((readLength = is.read(buffer)) != -1) {
105 | totalReadLength += readLength;
106 | os.write(buffer, 0, readLength);
107 | if (isWritePause(totalReadLength, responseBodyLength) && !hasPaused) {
108 | pause(writeBodyPause);
109 | hasPaused = true;
110 | LOGGER.info("Continue to write " + Instant.now());
111 | }
112 | }
113 | }
114 |
115 | os.flush();
116 | }
117 |
118 | private boolean isWritePause(int length, long totalLength) {
119 | return writeBodyPause.toMillis() > 0 && length >= totalLength * 0.90;
120 | }
121 |
122 | @SuppressWarnings("java:S2925")
123 | private void pause(Duration duration) {
124 | try {
125 | TimeUnit.MILLISECONDS.sleep(duration.toMillis());
126 | } catch (InterruptedException ignore) {
127 | Thread.currentThread().interrupt();
128 | }
129 | }
130 |
131 | }
132 |
133 | /**
134 | * Builder for RSS server
135 | */
136 | public static class RssServerBuilder {
137 | private int port = 8080;
138 | private String endpointPath = "/rss";
139 | private final File file;
140 | private final Duration writeBodyPause;
141 |
142 | RssServerBuilder(File file, Duration writeBodyPause) {
143 | this.file = file;
144 | this.writeBodyPause = writeBodyPause;
145 | }
146 |
147 | /**
148 | * Port number to use. Default: 8080
149 | * @param port port number
150 | * @return builder
151 | */
152 | public RssServerBuilder port(int port) {
153 | this.port = port;
154 | return this;
155 | }
156 |
157 | /**
158 | * The endpoint path to use. Default: /rss
159 | * @param endpointPath endpoint path
160 | * @return builder
161 | */
162 | public RssServerBuilder endpointPath(String endpointPath) {
163 | this.endpointPath = endpointPath;
164 | return this;
165 | }
166 |
167 | /**
168 | * Builds and configures the RSS server
169 | * @return RSS server
170 | * @throws IOException if an I/O error occurs
171 | */
172 | public RssServer build() throws IOException {
173 | return new RssServer(port, endpointPath, file, writeBodyPause);
174 | }
175 | }
176 |
177 | }
178 |
--------------------------------------------------------------------------------
/src/test/java/com/apptasticsoftware/rssreader/module/itunes/ItunesRssReaderTest.java:
--------------------------------------------------------------------------------
1 | package com.apptasticsoftware.rssreader.module.itunes;
2 |
3 | import com.apptasticsoftware.rssreader.DateTime;
4 | import com.apptasticsoftware.rssreader.util.ItemComparator;
5 | import nl.jqno.equalsverifier.EqualsVerifier;
6 | import org.junit.jupiter.api.Test;
7 |
8 | import javax.net.ssl.SSLContext;
9 | import java.io.IOException;
10 | import java.io.InputStream;
11 | import java.net.http.HttpClient;
12 | import java.security.KeyManagementException;
13 | import java.security.NoSuchAlgorithmException;
14 | import java.time.Duration;
15 | import java.util.stream.Collectors;
16 |
17 | import static org.junit.jupiter.api.Assertions.*;
18 |
19 | class ItunesRssReaderTest {
20 |
21 | @Test
22 | void readItunesPodcastFeed() {
23 | var res = new ItunesRssReader().read(fromFile("itunes-podcast.xml"))
24 | .sorted(ItemComparator.oldestPublishedItemFirst())
25 | .collect(Collectors.toList());
26 |
27 | assertEquals(9, res.size());
28 | }
29 |
30 | @Test
31 | void readItunesPodcastFeedFromUrl() throws IOException {
32 | var res = new ItunesRssReader().read("https://feeds.theincomparable.com/batmanuniversity")
33 | .collect(Collectors.toList());
34 |
35 | assertFalse(res.isEmpty());
36 | }
37 |
38 | @Test
39 | void httpClient() throws IOException, KeyManagementException, NoSuchAlgorithmException {
40 | SSLContext context = SSLContext.getInstance("TLSv1.3");
41 | context.init(null, null, null);
42 |
43 | HttpClient httpClient = HttpClient.newBuilder()
44 | .sslContext(context)
45 | .connectTimeout(Duration.ofSeconds(15))
46 | .followRedirects(HttpClient.Redirect.NORMAL)
47 | .build();
48 |
49 | var res = new ItunesRssReader(httpClient).read("https://feeds.theincomparable.com/batmanuniversity")
50 | .collect(Collectors.toList());
51 |
52 | assertFalse(res.isEmpty());
53 | }
54 |
55 | @Test
56 | void equalsContract() {
57 | EqualsVerifier.simple().forClass(ItunesChannel.class).withIgnoredFields("dateTimeParser").withIgnoredFields("category").withNonnullFields("categories").withIgnoredFields("syUpdatePeriod").withIgnoredFields("syUpdateFrequency").withNonnullFields("itunesCategories").verify();
58 | EqualsVerifier.simple().forClass(ItunesItem.class).withIgnoredFields("defaultComparator").withIgnoredFields("dateTimeParser").withIgnoredFields("category").withNonnullFields("categories").withIgnoredFields("enclosure").withNonnullFields("enclosures").verify();
59 | EqualsVerifier.simple().forClass(ItunesOwner.class).verify();
60 | }
61 |
62 | @Test
63 | void duration() {
64 | ItunesItem item = new ItunesItem(new DateTime());
65 | item.setItunesDuration("1");
66 | assertEquals(1, item.getItunesDurationAsDuration().get().getSeconds());
67 | item.setItunesDuration("01:02");
68 | assertEquals(62, item.getItunesDurationAsDuration().get().getSeconds());
69 | item.setItunesDuration("01:02:03");
70 | assertEquals(3723, item.getItunesDurationAsDuration().get().getSeconds());
71 | }
72 |
73 | @Test
74 | void badDuration() {
75 | ItunesItem item = new ItunesItem(new DateTime());
76 | item.setItunesDuration(null);
77 | assertTrue(item.getItunesDurationAsDuration().isEmpty());
78 | item.setItunesDuration(" ");
79 | assertTrue(item.getItunesDurationAsDuration().isEmpty());
80 | item.setItunesDuration(":");
81 | assertTrue(item.getItunesDurationAsDuration().isEmpty());
82 | item.setItunesDuration("a");
83 | assertTrue(item.getItunesDurationAsDuration().isEmpty());
84 | item.setItunesDuration("a:b");
85 | assertTrue(item.getItunesDurationAsDuration().isEmpty());
86 | item.setItunesDuration("a:b:c");
87 | assertTrue(item.getItunesDurationAsDuration().isEmpty());
88 | }
89 |
90 | private InputStream fromFile(String fileName) {
91 | return getClass().getClassLoader().getResourceAsStream(fileName);
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/test/java/com/apptasticsoftware/rssreader/module/mediarss/MediaRssReaderTest.java:
--------------------------------------------------------------------------------
1 | package com.apptasticsoftware.rssreader.module.mediarss;
2 |
3 | import com.apptasticsoftware.rssreader.util.ItemComparator;
4 | import nl.jqno.equalsverifier.EqualsVerifier;
5 | import org.junit.jupiter.api.Test;
6 |
7 | import java.io.InputStream;
8 | import java.util.stream.Collectors;
9 |
10 | import static com.github.npathai.hamcrestopt.OptionalMatchers.isEmpty;
11 | import static com.github.npathai.hamcrestopt.OptionalMatchers.isPresentAnd;
12 | import static org.hamcrest.MatcherAssert.assertThat;
13 | import static org.hamcrest.Matchers.equalTo;
14 | import static org.junit.jupiter.api.Assertions.assertEquals;
15 |
16 | class MediaRssReaderTest {
17 |
18 | @Test
19 | void readMediaRssFeed() {
20 | var res = new MediaRssReader().read(fromFile("media-rss.xml"))
21 | .collect(Collectors.toList());
22 |
23 | assertEquals(10, res.size());
24 | }
25 |
26 | @Test
27 | void readMediaRssFeedItemTitle() {
28 | var res = new MediaRssReader().read(fromFile("media-rss.xml"))
29 | .sorted(ItemComparator.oldestPublishedItemFirst())
30 | .collect(Collectors.toList());
31 |
32 | MediaRssItem item = res.get(0);
33 | assertThat(item.getTitle(), isPresentAnd(equalTo("Ignitis_wind")));
34 | }
35 |
36 | @Test
37 | void readMediaRssFeedItemPubDate() {
38 | var res = new MediaRssReader().read(fromFile("media-rss.xml"))
39 | .sorted(ItemComparator.oldestPublishedItemFirst())
40 | .collect(Collectors.toList());
41 |
42 | MediaRssItem item = res.get(0);
43 | assertThat(item.getPubDate(), isPresentAnd(equalTo("Mon, 07 Nov 2022 14:51:45 -0500")));
44 | }
45 |
46 | @Test
47 | void readMediaRssFeedItemLink() {
48 | var res = new MediaRssReader().read(fromFile("media-rss.xml"))
49 | .sorted(ItemComparator.oldestPublishedItemFirst())
50 | .collect(Collectors.toList());
51 |
52 | MediaRssItem item = res.get(0);
53 | assertThat(item.getLink(), isPresentAnd(equalTo("https://vimeo.com/768251452")));
54 | }
55 |
56 | @Test
57 | void readMediaRssFeedDescription() {
58 | var res = new MediaRssReader().read(fromFile("media-rss.xml"))
59 | .sorted(ItemComparator.oldestPublishedItemFirst())
60 | .collect(Collectors.toList());
61 |
62 | MediaRssItem item = res.get(0);
63 | assertThat(item.getDescription(), isPresentAnd(equalTo("This is "Ignitis_wind" by pvz.lt on Vimeo, the home for high quality videos and the people who love them.")));
64 | }
65 |
66 | @Test
67 | void readMediaRssFeedGuid() {
68 | var res = new MediaRssReader().read(fromFile("media-rss.xml"))
69 | .sorted(ItemComparator.oldestPublishedItemFirst())
70 | .collect(Collectors.toList());
71 |
72 | MediaRssItem item = res.get(0);
73 | assertThat(item.getGuid(), isPresentAnd(equalTo("tag:vimeo,2022-11-07:clip768251452")));
74 | }
75 |
76 | @Test
77 | void readMediaRssFeedIsPermaLink() {
78 | var res = new MediaRssReader().read(fromFile("media-rss.xml"))
79 | .sorted(ItemComparator.oldestPublishedItemFirst())
80 | .collect(Collectors.toList());
81 |
82 | MediaRssItem item = res.get(0);
83 | assertThat(item.getIsPermaLink(), isPresentAnd(equalTo(false)));
84 | }
85 |
86 | @Test
87 | void readMediaRssFeedThumbnail() {
88 | var res = new MediaRssReader().read(fromFile("media-rss.xml"))
89 | .sorted(ItemComparator.oldestPublishedItemFirst())
90 | .collect(Collectors.toList());
91 |
92 | MediaRssItem item = res.get(0);
93 | MediaThumbnail mediaThumbnail = item.getMediaThumbnail().get();
94 | assertEquals("https://i.vimeocdn.com/video/1542457228-31ab55501fdd5316663c63781ae1a37932abc4b314bcc619e3377c0ca85b859d-d_960", mediaThumbnail.getUrl());
95 | assertThat(mediaThumbnail.getHeight(), isPresentAnd(equalTo(540)));
96 | assertThat(mediaThumbnail.getWidth(), isPresentAnd(equalTo(960)));
97 | assertThat(mediaThumbnail.getTime(), isEmpty());
98 | }
99 |
100 | @Test
101 | void equalsContract() {
102 | EqualsVerifier.simple().forClass(MediaRssItem.class).withIgnoredFields("defaultComparator").withIgnoredFields("dateTimeParser").withIgnoredFields("category").withNonnullFields("categories").withIgnoredFields("enclosure").withNonnullFields("enclosures").verify();
103 | }
104 |
105 | private InputStream fromFile(String fileName) {
106 | return getClass().getClassLoader().getResourceAsStream(fileName);
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/src/test/java/com/apptasticsoftware/rssreader/util/ItemComparatorTest.java:
--------------------------------------------------------------------------------
1 | package com.apptasticsoftware.rssreader.util;
2 |
3 | import com.apptasticsoftware.rssreader.RssReader;
4 | import org.junit.jupiter.api.Test;
5 |
6 | import java.io.InputStream;
7 | import java.time.ZonedDateTime;
8 | import java.util.List;
9 | import java.util.Objects;
10 | import java.util.stream.Collectors;
11 |
12 | import static org.junit.jupiter.api.Assertions.assertTrue;
13 |
14 | @SuppressWarnings("java:S5738")
15 | class ItemComparatorTest {
16 |
17 | @Test
18 | void testSortNewestItem() {
19 | var items = new RssReader().read(fromFile("item-sort-test.xml"))
20 | .sorted(ItemComparator.newestItemFirst())
21 | .map(i -> i.getPubDateZonedDateTime().orElse(null))
22 | .filter(Objects::nonNull)
23 | .map(ZonedDateTime::toEpochSecond)
24 | .collect(Collectors.toList());
25 |
26 | assertTrue(isDescendingSortOrder(items));
27 | }
28 |
29 | @Test
30 | void testSortNewestPublishedItem() {
31 | var items = new RssReader().read(fromFile("item-sort-test.xml"))
32 | .sorted(ItemComparator.newestPublishedItemFirst())
33 | .map(i -> i.getPubDateZonedDateTime().orElse(null))
34 | .filter(Objects::nonNull)
35 | .map(ZonedDateTime::toEpochSecond)
36 | .collect(Collectors.toList());
37 |
38 | assertTrue(isDescendingSortOrder(items));
39 | }
40 |
41 | @Test
42 | void testSortNewestItemWithCustomDateTimeParser() {
43 | var items = new RssReader().setDateTimeParser(Default.getDateTimeParser())
44 | .read(fromFile("item-sort-test.xml"))
45 | .sorted(ItemComparator.newestItemFirst())
46 | .map(i -> i.getPubDateZonedDateTime().orElse(null))
47 | .filter(Objects::nonNull)
48 | .map(ZonedDateTime::toEpochSecond)
49 | .collect(Collectors.toList());
50 |
51 | assertTrue(isDescendingSortOrder(items));
52 | }
53 |
54 | @Test
55 | void testSortNewestPublishedItemWithCustomDateTimeParser() {
56 | var items = new RssReader().setDateTimeParser(Default.getDateTimeParser())
57 | .read(fromFile("item-sort-test.xml"))
58 | .sorted(ItemComparator.newestPublishedItemFirst())
59 | .map(i -> i.getPubDateZonedDateTime().orElse(null))
60 | .filter(Objects::nonNull)
61 | .map(ZonedDateTime::toEpochSecond)
62 | .collect(Collectors.toList());
63 |
64 | assertTrue(isDescendingSortOrder(items));
65 | }
66 |
67 | @Test
68 | void testSortNewestItemWithDateTimeParser() {
69 | var items = new RssReader().read(fromFile("item-sort-test.xml"))
70 | .sorted(ItemComparator.newestItemFirst(Default.getDateTimeParser()))
71 | .map(i -> i.getPubDateZonedDateTime().orElse(null))
72 | .filter(Objects::nonNull)
73 | .map(ZonedDateTime::toEpochSecond)
74 | .collect(Collectors.toList());
75 |
76 | assertTrue(isDescendingSortOrder(items));
77 | }
78 |
79 | @Test
80 | void testSortNewestPublishedItemWithDateTimeParser() {
81 | var items = new RssReader().read(fromFile("item-sort-test.xml"))
82 | .sorted(ItemComparator.newestPublishedItemFirst(Default.getDateTimeParser()))
83 | .map(i -> i.getPubDateZonedDateTime().orElse(null))
84 | .filter(Objects::nonNull)
85 | .map(ZonedDateTime::toEpochSecond)
86 | .collect(Collectors.toList());
87 |
88 | assertTrue(isDescendingSortOrder(items));
89 | }
90 |
91 | @Test
92 | void testSortOldestItemFirst() {
93 | var items = new RssReader().read(fromFile("item-sort-test.xml"))
94 | .sorted(ItemComparator.oldestItemFirst())
95 | .map(i -> i.getPubDateZonedDateTime().orElse(null))
96 | .filter(Objects::nonNull)
97 | .collect(Collectors.toList());
98 |
99 | assertTrue(isAscendingSortOrder(items));
100 | }
101 |
102 | @Test
103 | void testSortOldestPublishedItemFirst() {
104 | var items = new RssReader().read(fromFile("item-sort-test.xml"))
105 | .sorted(ItemComparator.oldestPublishedItemFirst())
106 | .map(i -> i.getPubDateZonedDateTime().orElse(null))
107 | .filter(Objects::nonNull)
108 | .collect(Collectors.toList());
109 |
110 | assertTrue(isAscendingSortOrder(items));
111 | }
112 |
113 | @Test
114 | void testSortOldestItemFirstWithDateTimeParser() {
115 | var items = new RssReader().read(fromFile("item-sort-test.xml"))
116 | .sorted(ItemComparator.oldestItemFirst(Default.getDateTimeParser()))
117 | .map(i -> i.getPubDateZonedDateTime().orElse(null))
118 | .filter(Objects::nonNull)
119 | .collect(Collectors.toList());
120 |
121 | assertTrue(isAscendingSortOrder(items));
122 | }
123 |
124 | @Test
125 | void testSortOldestPublishedItemFirstWithDateTimeParser() {
126 | var items = new RssReader().read(fromFile("item-sort-test.xml"))
127 | .sorted(ItemComparator.oldestPublishedItemFirst(Default.getDateTimeParser()))
128 | .map(i -> i.getPubDateZonedDateTime().orElse(null))
129 | .filter(Objects::nonNull)
130 | .collect(Collectors.toList());
131 |
132 | assertTrue(isAscendingSortOrder(items));
133 | }
134 |
135 | @Test
136 | void testSortNewestUpdatedItem() {
137 | var items = new RssReader().read(fromFile("item-sort-test.xml"))
138 | .sorted(ItemComparator.newestUpdatedItemFirst())
139 | .map(i -> i.getUpdatedZonedDateTime().orElse(null))
140 | .filter(Objects::nonNull)
141 | .map(ZonedDateTime::toEpochSecond)
142 | .collect(Collectors.toList());
143 |
144 | assertTrue(isDescendingSortOrder(items));
145 | }
146 |
147 | @Test
148 | void testSortNewestUpdatedItemWithCustomDateTimeParser() {
149 | var items = new RssReader().setDateTimeParser(Default.getDateTimeParser())
150 | .read(fromFile("item-sort-test.xml"))
151 | .sorted(ItemComparator.newestUpdatedItemFirst())
152 | .map(i -> i.getUpdatedZonedDateTime().orElse(null))
153 | .filter(Objects::nonNull)
154 | .map(ZonedDateTime::toEpochSecond)
155 | .collect(Collectors.toList());
156 |
157 | assertTrue(isDescendingSortOrder(items));
158 | }
159 |
160 | @Test
161 | void testSortNewestUpdatedItemWithDateTimeParser() {
162 | var items = new RssReader().read(fromFile("item-sort-test.xml"))
163 | .sorted(ItemComparator.newestUpdatedItemFirst(Default.getDateTimeParser()))
164 | .map(i -> i.getUpdatedZonedDateTime().orElse(null))
165 | .filter(Objects::nonNull)
166 | .map(ZonedDateTime::toEpochSecond)
167 | .collect(Collectors.toList());
168 |
169 | assertTrue(isDescendingSortOrder(items));
170 | }
171 |
172 | @Test
173 | void testSortOldestUpdatedItemFirst() {
174 | var items = new RssReader().read(fromFile("item-sort-test.xml"))
175 | .sorted(ItemComparator.oldestUpdatedItemFirst())
176 | .map(i -> i.getUpdatedZonedDateTime().orElse(null))
177 | .filter(Objects::nonNull)
178 | .collect(Collectors.toList());
179 |
180 | assertTrue(isAscendingSortOrder(items));
181 | }
182 |
183 | @Test
184 | void testSortOldestUpdatedItemFirstWithDateTimeParser() {
185 | var items = new RssReader().read(fromFile("item-sort-test.xml"))
186 | .sorted(ItemComparator.oldestUpdatedItemFirst(Default.getDateTimeParser()))
187 | .map(i -> i.getUpdatedZonedDateTime().orElse(null))
188 | .filter(Objects::nonNull)
189 | .collect(Collectors.toList());
190 |
191 | assertTrue(isAscendingSortOrder(items));
192 | }
193 |
194 | @Test
195 | void testSortChannelTitle() {
196 | var urlList = List.of("https://www.theverge.com/rss/reviews/index.xml", "https://feeds.macrumors.com/MacRumors-All");
197 | var items = new RssReader().read(urlList)
198 | .sorted(ItemComparator.channelTitle())
199 | .map(i -> i.getChannel().getTitle())
200 | .filter(Objects::nonNull)
201 | .collect(Collectors.toList());
202 |
203 | assertTrue(isAscendingSortOrder(items));
204 | }
205 |
206 |
207 | private static > boolean isAscendingSortOrder(List array){
208 | for (int i = 0; i < array.size()-1; i++) {
209 | if (array.get(i).compareTo(array.get(i+1)) > 0){
210 | return false;
211 | }
212 | }
213 | return true;
214 | }
215 |
216 | private static > boolean isDescendingSortOrder(List array){
217 | for (int i = 0; i < array.size()-1; i++) {
218 | if (array.get(i).compareTo(array.get(i+1)) < 0){
219 | return false;
220 | }
221 | }
222 | return true;
223 | }
224 |
225 | private InputStream fromFile(String fileName) {
226 | return getClass().getClassLoader().getResourceAsStream(fileName);
227 | }
228 |
229 | }
230 |
--------------------------------------------------------------------------------
/src/test/java/com/apptasticsoftware/rssreader/util/MapperTest.java:
--------------------------------------------------------------------------------
1 | package com.apptasticsoftware.rssreader.util;
2 |
3 | import com.apptasticsoftware.rssreader.*;
4 | import org.junit.jupiter.api.Test;
5 | import org.junit.jupiter.params.ParameterizedTest;
6 | import org.junit.jupiter.params.provider.Arguments;
7 | import org.junit.jupiter.params.provider.MethodSource;
8 | import org.junit.jupiter.params.provider.ValueSource;
9 |
10 | import java.util.Optional;
11 | import java.util.logging.Level;
12 | import java.util.logging.Logger;
13 | import java.util.stream.Stream;
14 |
15 | import static com.apptasticsoftware.rssreader.util.Mapper.mapInteger;
16 | import static org.junit.jupiter.api.Assertions.*;
17 |
18 | class MapperTest {
19 |
20 | @ParameterizedTest
21 | @ValueSource(strings = {"true", "TRUE", "True", "yes", "YES", "Yes"})
22 | void testMapBooleanTrue(String trueValue) {
23 | Item item = new Item(new DateTime());
24 | Mapper.mapBoolean(trueValue, item::setIsPermaLink);
25 | assertEquals(true, item.getIsPermaLink().orElse(null));
26 | }
27 |
28 | @ParameterizedTest
29 | @ValueSource(strings = {"false", "FALSE", "False", "no", "NO", "No"})
30 | void testMapBooleanFalse(String falseValue) {
31 | Item item = new Item(new DateTime());
32 | Mapper.mapBoolean(falseValue, item::setIsPermaLink);
33 | assertEquals(false, item.getIsPermaLink().orElse(null));
34 | }
35 |
36 | @ParameterizedTest
37 | @ValueSource(strings = {"Bad value", ""})
38 | void testMapBooleanBadValue(String falseValue) {
39 | Item item = new Item(new DateTime());
40 | Mapper.mapBoolean(falseValue, item::setIsPermaLink);
41 | assertNull(item.getIsPermaLink().orElse(null));
42 | }
43 |
44 | @ParameterizedTest
45 | @ValueSource(strings = {"1", "-1", "0", "12345", "-12345"})
46 | void testMapInt(String intTextValue) {
47 | Image image = new Image();
48 | Mapper.mapInteger(intTextValue, image::setHeight);
49 | assertEquals(Integer.valueOf(intTextValue), image.getHeight().orElse(null));
50 | }
51 |
52 | @ParameterizedTest
53 | @ValueSource(strings = {"aaa", "a1", "1a"})
54 | void testMapBadInt(String intTextValue) {
55 | Image image = new Image();
56 | Mapper.mapInteger(intTextValue, image::setHeight);
57 | assertTrue(image.getHeight().isEmpty());
58 | }
59 |
60 | @ParameterizedTest
61 | @ValueSource(strings = {"1", "-1", "0", "12345", "-12345"})
62 | void testMapLong(String longTextValue) {
63 | Enclosure enclosure = new Enclosure();
64 | Mapper.mapLong(longTextValue, enclosure::setLength);
65 | assertEquals(Long.valueOf(longTextValue), enclosure.getLength().orElse(null));
66 | }
67 |
68 | @ParameterizedTest
69 | @ValueSource(strings = {"aaa", "a1", "1a"})
70 | void testMapBadLong(String longTextValue) {
71 | Enclosure enclosure = new Enclosure();
72 | Mapper.mapLong(longTextValue, enclosure::setLength);
73 | assertTrue(enclosure.getLength().isEmpty());
74 | }
75 |
76 |
77 | @Test
78 | void testCreateIfNull() {
79 | Channel channel = new Channel(new DateTime());
80 | Mapper.createIfNull(channel::getImage, channel::setImage, Image::new).setTitle("title");
81 | assertEquals("title", channel.getImage().map(Image::getTitle).orElse("-"));
82 | Mapper.createIfNull(channel::getImage, channel::setImage, Image::new).setUrl("url");
83 | assertEquals("url", channel.getImage().map(Image::getUrl).orElse("-"));
84 | assertEquals("title", channel.getImage().map(Image::getTitle).orElse("-"));
85 | }
86 |
87 | @Test
88 | void testCreateIfNullOptional() {
89 | Channel channel = new Channel(new DateTime());
90 | Mapper.createIfNullOptional(channel::getImage, channel::setImage, Image::new).ifPresent(i -> mapInteger("200", i::setHeight));
91 | assertEquals(200, channel.getImage().flatMap(Image::getHeight).orElse(0));
92 | Mapper.createIfNullOptional(channel::getImage, channel::setImage, Image::new).ifPresent(i -> mapInteger("100", i::setWidth));
93 | assertEquals(100, channel.getImage().flatMap(Image::getWidth).orElse(0));
94 | assertEquals(200, channel.getImage().flatMap(Image::getHeight).orElse(0));
95 | }
96 |
97 | @Test
98 | void testBadNumberLogging() {
99 | var logger = Logger.getLogger("com.apptasticsoftware.rssreader.util");
100 | logger.setLevel(Level.ALL);
101 |
102 | var image = new Image();
103 | Mapper.mapInteger("-", image::setHeight);
104 | assertEquals(Optional.empty(), image.getHeight());
105 |
106 | logger.setLevel(Level.OFF);
107 | Mapper.mapInteger("-", image::setHeight);
108 | assertEquals(Optional.empty(), image.getHeight());
109 |
110 | Mapper.mapInteger("", image::setHeight);
111 | assertEquals(Optional.empty(), image.getHeight());
112 |
113 | Mapper.mapInteger(null, image::setHeight);
114 | assertEquals(Optional.empty(), image.getHeight());
115 | }
116 |
117 | @ParameterizedTest
118 | @MethodSource("mapIfEmptyParameters")
119 | void testMapIfEmpty(TestObject testObject, String value, String expected) {
120 | Mapper.mapIfEmpty(value, testObject::getText, testObject::setText);
121 | assertEquals(expected, testObject.getText());
122 | }
123 |
124 | @ParameterizedTest
125 | @MethodSource("mapIfEmptyParameters")
126 | void testOptionalMapIfEmpty(TestObject testObject, String value, String expected) {
127 | Mapper.mapIfEmpty(value, testObject::getOptionalText, testObject::setText);
128 | assertEquals(expected, testObject.getText());
129 | }
130 |
131 | private static Stream mapIfEmptyParameters() {
132 | return Stream.of(
133 | Arguments.of(new TestObject(null), "value", "value"),
134 | Arguments.of(new TestObject(""), "value", "value"),
135 | Arguments.of(new TestObject(null), "", null),
136 | Arguments.of(new TestObject(null), null, null),
137 | Arguments.of(new TestObject(""), "", ""),
138 | Arguments.of(new TestObject(""), null, ""),
139 | Arguments.of(new TestObject("value"), "other value", "value")
140 | );
141 | }
142 |
143 |
144 | static class TestObject {
145 | private String text;
146 |
147 | public TestObject(String value) {
148 | text = value;
149 | }
150 |
151 | public void setText(String value) {
152 | text = value;
153 | }
154 |
155 | public String getText() {
156 | return text;
157 | }
158 |
159 | public Optional getOptionalText() {
160 | return Optional.ofNullable(text);
161 | }
162 |
163 | }
164 |
165 | }
166 |
--------------------------------------------------------------------------------
/src/test/java/com/apptasticsoftware/rssreader/util/UtilTest.java:
--------------------------------------------------------------------------------
1 | package com.apptasticsoftware.rssreader.util;
2 |
3 | import org.junit.jupiter.params.ParameterizedTest;
4 | import org.junit.jupiter.params.provider.Arguments;
5 | import org.junit.jupiter.params.provider.MethodSource;
6 |
7 | import java.util.stream.Stream;
8 |
9 | import static org.junit.jupiter.api.Assertions.assertEquals;
10 |
11 | class UtilTest {
12 |
13 | @ParameterizedTest(name = "{0} is expected to output {1}")
14 | @MethodSource("periodToHoursTestData")
15 | void periodToHours(String period, int expectedHours) {
16 | assertEquals(expectedHours, Util.toMinutes(period));
17 | }
18 |
19 | private static Stream periodToHoursTestData() {
20 | return Stream.of(
21 | Arguments.of("daily", 1440),
22 | Arguments.of("weekly", 10080),
23 | Arguments.of("monthly", 43800),
24 | Arguments.of("yearly", 525600),
25 | Arguments.of("hourly", 60),
26 | Arguments.of("unknown", 60)
27 | );
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/src/test/resources/atom-feed-category.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | FYI Center for Software Developers
4 | FYI (For Your Information) Center for Software Developers with
5 | large collection of FAQs, tutorials and tips codes for application and
6 | wWeb developers on Java, .NET, C, PHP, JavaScript, XML, HTML, CSS, RSS,
7 | MySQL and Oracle - dev.fyicenter.com.
8 | https://example.com/logo.png
9 |
10 | http://dev.fyicenter.com/atom_xml.php
11 | 2017-09-22T03:58:52+02:00
12 |
13 | FYIcenter.com
14 |
15 | Copyright (c) 2017 FYIcenter.com
16 |
17 |
18 |
19 |
20 | Use Developer Portal Internally
21 |
24 |
25 | http://dev.fyicenter.com/1000702_Use_Developer_Portal_Internally.html
26 |
27 | 2017-09-20T13:29:08+02:00
28 | <img align='left' width='64' height='64'
29 | src='http://dev.fyicenter.com/Azure-API/_icon_Azure-API.png' />How to
30 | use the Developer Portal internally by you as the publisher? Normally,
31 | the Developer Portal of an Azure API Management Service is used by
32 | client developers. But as a publisher, you can also use the Developer
33 | Portal to test API operations internally. You can follow this tutorial
34 | to access the ... - Rank: 120; Updated: 2017-09-20 13:29:06 -> <a
35 | href='http://dev.fyicenter.com/1000702_Use_Developer_Portal_Internally.ht
36 | ml'>Source</a>
37 |
38 | FYIcenter.com
39 |
40 |
41 |
42 |
43 |
44 | Using Azure API Management Developer Portal
45 |
48 |
49 | http://dev.fyicenter.com/1000701_Using_Azure_API_Management_Developer
50 | _Portal.html
51 | 2017-09-20T13:29:07+02:00
52 | <img align='left' width='64' height='64'
53 | src='http://dev.fyicenter.com/Azure-API/_icon_Azure-API.png' />Where to
54 | find tutorials on Using Azure API Management Developer Portal? Here is
55 | a list of tutorials to answer many frequently asked questions compiled
56 | by FYIcenter.com team on Using Azure API Management Developer Portal:
57 | Use Developer Portal Internally What Can I See on Developer Portal What
58 | I You T... - Rank: 120; Updated: 2017-09-20 13:29:06 -> <a
59 | href='http://dev.fyicenter.com/1000701_Using_Azure_API_Management_Develop
60 | er_Portal.html'>Source</a>
61 |
62 | FYIcenter.com
63 |
64 |
65 |
66 |
67 | Add API to API Products
68 |
70 | http://dev.fyicenter.com/1000700_Add_API_to_API_Products.html
71 | 2017-09-20T13:29:06+02:00
72 | <img align='left' width='64' height='64'
73 | src='http://dev.fyicenter.com/Azure-API/_icon_Azure-API.png' />How to
74 | add an API to an API product for internal testing on the Publisher
75 | Portal of an Azure API Management Service? You can follow this tutorial
76 | to add an API to an API product on the Publisher Portal of an Azure API
77 | Management Service. 1. Click API from the left menu on the Publisher
78 | Portal. You s... - Rank: 119; Updated: 2017-09-20 13:29:06 -> <a
79 | href='http://dev.fyicenter.com/1000700_Add_API_to_API_Products.html'>Sour
80 | ce</a>
81 |
82 | FYIcenter.com
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/src/test/resources/atom-feed.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | dive into mark
4 |
5 | A <em>lot</em> of effort
6 | went into making this effortless
7 |
8 | https://example.com/icon.png
9 | 2005-07-31T12:29:29Z
10 | tag:example.org,2003:3
11 |
13 |
15 | Copyright (c) 2003, Mark Pilgrim
16 |
17 | Example Toolkit
18 |
19 |
20 | Atom draft-07 snapshot
21 |
23 |
25 | tag:example.org,2003:3.2397
26 | 2005-07-31T12:29:29Z
27 | 2003-12-13T08:29:29-04:00
28 |
29 | Mark Pilgrim
30 | http://example.org/
31 | f8dy@example.com
32 |
33 |
34 | Sam Ruby
35 |
36 |
37 | Joe Gregorio
38 |
39 |
41 |
42 |
[Update: The Atom draft is finished.]
43 |
44 |
45 |
46 |
47 |
48 |
50 | 10
51 | John
52 | Doe
53 |
54 |
55 | Atom-Powered Robots Run Amok
56 |
57 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6a
58 | 2003-12-13T18:30:02Z
59 |
60 |
61 |
62 | {"firstName"="John","lastName"="Doe","id"="10"}
63 |
64 | Atom-Powered Robots Run Amok 2
65 |
66 | urn:uuid:1225c695-cfb8-4ebb-aaaa-80da344efa6b
67 | 2003-12-13T09:28:28-04:00
68 | 2003-12-13T18:30:01Z
69 |
70 |
--------------------------------------------------------------------------------
/src/test/resources/bad-image-width-height.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | title
5 | https://test.com/
6 | Test channel
7 | testing
8 | Tue, 29 Nov 2022 13:49:44 +0100
9 | Tue, 29 Nov 2022 13:49:44 +0100
10 | en
11 | https://test.com/generator
12 | testing@test.com
13 | webmaster@test.com
14 | 120
15 |
16 | https://www.test.com/testing.jpg
17 | testing
18 |
19 | not-a-number
20 |
21 |
22 |
23 | test item 1
24 | Sun, 27 Nov 2022 00:00:00 +0100
25 |
26 | test-id-1
27 | A test item
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/src/test/resources/empty-category.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | GameSpot - All News
4 | https://www.gamespot.com/feeds/news
5 | The latest News from GameSpot
6 | en-us
7 | Sun, 04 Dec 2022 23:33:42 -0800
8 |
9 |
10 |
11 | Today's Wordle Answer (#534) - December 5, 2022
12 | https://www.gamespot.com/articles/todays-wordle-answer-534-december-5-2022/1100-6509697/?ftag=CAD-01-10abi2f
13 |
14 | It's Monday and that can only mean one thing: We're back for another week of Wordle guides. After a long weekend, it's time to get back into the swing of things, and we're here to make sure you do that by getting the Wordle correct. Today's answer doesn't do players any favors, as it's definitely not a word that many users will think of quickly. If you haven't started the December 5 Wordle just yet, then you can check out our list of recommended starting words. However, if you're already past the starting point and visiting this article, then you might be in need of some help.
That's where we can come in. Below, players will find two tips for today's Wordle. We've also spelled out the full answer for players who might not find the hints helpful enough.
Today's Wordle Answer - December 5, 2022
We'll begin with a couple of hints that directly relate to the answer, but won't give it away.
Vous vous passionnez pour les images satellites et vous aimeriez bien faire un timelapse d’une zone particulière de la planète pour montrer son évolution ?
Et bien avec Streamlit c’est possible. Le principe est simple. Vous sélectionnez une zone sur la carte, vous exportez cette zone dans un fichier json. Vous réimportez ensuite ce json, vous choisissez une collection d’images satellites et vous cliquez sur le bouton « Submit ».
Et voilà, vous aurez un joli GIF animé ou MP4 à télécharger. Je vous laisse regarder les vidéos pour voir ce que ça donne.