001package ball.tv.epg.sd;
002/*-
003 * ##########################################################################
004 * TV H/W, EPGs, and Recording
005 * $Id: SDClient.java 7215 2021-01-03 18:39:51Z ball $
006 * $HeadURL: svn+ssh://svn.hcf.dev/var/spool/scm/repository.svn/ball-tv/trunk/src/main/java/ball/tv/epg/sd/SDClient.java $
007 * %%
008 * Copyright (C) 2013 - 2021 Allen D. Ball
009 * %%
010 * Licensed under the Apache License, Version 2.0 (the "License");
011 * you may not use this file except in compliance with the License.
012 * You may obtain a copy of the License at
013 *
014 *      http://www.apache.org/licenses/LICENSE-2.0
015 *
016 * Unless required by applicable law or agreed to in writing, software
017 * distributed under the License is distributed on an "AS IS" BASIS,
018 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
019 * See the License for the specific language governing permissions and
020 * limitations under the License.
021 * ##########################################################################
022 */
023import ball.http.ProtocolClient;
024import ball.tv.ObjectMapperConfiguration;
025import ball.tv.epg.entity.Headend;
026import ball.tv.epg.entity.Lineup;
027import ball.tv.epg.entity.Program;
028import com.fasterxml.jackson.databind.JsonNode;
029import com.fasterxml.jackson.databind.node.ObjectNode;
030import java.io.IOException;
031import java.math.BigInteger;
032import java.net.URI;
033import java.nio.file.Files;
034import java.nio.file.Path;
035import java.nio.file.Paths;
036import java.security.MessageDigest;
037import java.time.Duration;
038import java.time.Instant;
039import java.util.Collection;
040import java.util.LinkedHashMap;
041import java.util.List;
042import java.util.Map;
043import org.apache.http.HttpEntity;
044import org.apache.http.HttpRequest;
045import org.apache.http.HttpResponse;
046import org.apache.http.ProtocolException;
047import org.apache.http.impl.client.DefaultRedirectStrategy;
048import org.apache.http.impl.client.HttpClientBuilder;
049import org.apache.http.protocol.HttpContext;
050
051import static java.nio.charset.StandardCharsets.UTF_8;
052
053/**
054 * {@link SDProtocol} client.
055 *
056 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
057 * @version $Revision: 7215 $
058 */
059public class SDClient extends ProtocolClient<SDProtocol> {
060    private static final Path PARENT =
061        Paths.get(System.getProperty("user.home"), ".tv", "epg", "sd")
062        .normalize();
063
064    private static final HttpClientBuilder BUILDER =
065        HttpClientBuilder.create()
066        .setRedirectStrategy(DefaultRedirectStrategy.INSTANCE);
067
068    private final Path parent;
069    private final Path credentials;
070    private final Path token;
071
072    /**
073     * No-argument constructor.
074     */
075    public SDClient() { this(null); }
076
077    /**
078     * Constructor to specify local parent directory.
079     *
080     * @param   parent          The local parent {@link Path}.
081     */
082    public SDClient(Path parent) { this(parent, null); }
083
084    /**
085     * Protected constructor.
086     *
087     * @param   parent          The local parent {@link Path}.
088     * @param   builder         A configured {@link HttpClientBuilder}.
089     */
090    protected SDClient(Path parent, HttpClientBuilder builder) {
091        super((builder != null) ? builder : BUILDER,
092              null, SDProtocol.class);
093        super.mapper = ObjectMapperConfiguration.MAPPER;
094
095        this.parent = (parent != null) ? parent : PARENT;
096        this.credentials = this.parent.resolve("credentials.json");
097        this.token = this.parent.resolve("token");
098    }
099
100    /**
101     * Method to get the local parent {@link Path}.
102     *
103     * @return  The parent {@link Path}.
104     */
105    public Path getParent() { return parent; }
106
107    /**
108     * Method to initialize the user configuration.
109     *
110     * @param   username        The user's name.
111     * @param   password        The user's password.
112     *
113     * @throws  Exception       If the hierarchy cannot be initialized.
114     */
115    public void initialize(String username, String password) throws Exception {
116        ObjectNode node = mapper.createObjectNode();
117
118        node.put("username", username);
119        node.put("password", hash(password));
120
121        Files.createDirectories(credentials.getParent());
122        mapper.writeValue(credentials.toFile(), node);
123    }
124
125    /**
126     * Method to invoke {@link SDProtocol#postToken(File)}.
127     *
128     * @return  The token {@link String} if successful; {@code null}
129     *          otherwise.
130     *
131     * @throws  Exception       If an exception is encountered invoking
132     *                          {@link SDProtocol#postToken(File)}.
133     */
134    public String getToken() throws Exception {
135        String value = null;
136
137        synchronized (this) {
138            if (Files.exists(token) && ageOf(token).toMinutes() > (24 * 60)) {
139                Files.deleteIfExists(token);
140            }
141
142            if (Files.exists(token)) {
143                value = new String(Files.readAllBytes(token), UTF_8);
144            } else {
145                SDProtocol.PostTokenResponse response =
146                    proxy().postToken(credentials.toFile());
147
148                if (response.getCode() == 0) {
149                    value = response.getToken();
150
151                    if (value != null) {
152                        Files.write(token, value.getBytes(UTF_8));
153                    }
154                } else {
155                    throw new ProtocolException(response.getMessage());
156                }
157            }
158        }
159
160        return value;
161    }
162
163    private Duration ageOf(Path path) throws Exception {
164        return Duration.between(Files.getLastModifiedTime(path).toInstant(),
165                                Instant.now());
166    }
167
168    /**
169     * Method to invoke {@link SDProtocol#getStatus(String)}.
170     *
171     * @return  The {@link SDProtocol.GetStatusResponse}.
172     *
173     * @throws  Exception       If an exception is encountered invoking
174     *                          {@link SDProtocol#getStatus(String)}.
175     */
176    public SDProtocol.GetStatusResponse getStatus() throws Exception {
177        return proxy().getStatus(getToken());
178    }
179
180    /**
181     * Method to invoke {@link SDProtocol#getVersion(String)}.
182     *
183     * @return  The {@link SDProtocol.GetVersionResponse}.
184     *
185     * @throws  Exception       If an exception is encountered invoking
186     *                          {@link SDProtocol#getVersion(String)}.
187     */
188    public SDProtocol.GetVersionResponse getVersion() throws Exception {
189        return proxy().getVersion(getClass().getPackage().getName());
190    }
191
192    /**
193     * Method to invoke
194     * {@link SDProtocol#getHeadends(String,String,String)}.
195     *
196     * @param   country         The country.
197     * @param   postalcode      The postal (ZIP) code.
198     *
199     * @return  The {@link Headend}s.
200     *
201     * @throws  Exception       If an exception is encountered invoking
202     *                          {@link SDProtocol#getHeadends(String,String,String)}.
203     */
204    public List<Headend> getHeadends(String country,
205                                     String postalcode) throws Exception {
206        return proxy().getHeadends(getToken(), country, postalcode);
207    }
208
209    /**
210     * Method to invoke {@link SDProtocol#putLineup(String,String)}.
211     *
212     * @param   lineup          The line-up to add.
213     *
214     * @return  The {@link JsonNode}.
215     *
216     * @throws  Exception       If an exception is encountered invoking
217     *                          {@link SDProtocol#putLineup(String,String)}.
218     */
219    public JsonNode putLineup(String lineup) throws Exception {
220        return proxy().putLineup(getToken(), lineup);
221    }
222
223    /**
224     * Method to invoke {@link SDProtocol#getLineups(String)}.
225     *
226     * @return  The {@link SDProtocol.GetLineupsResponse}.
227     *
228     * @throws  Exception       If an exception is encountered invoking
229     *                          {@link SDProtocol#getLineups(String)}.
230     */
231    public SDProtocol.GetLineupsResponse getLineups() throws Exception {
232        return proxy().getLineups(getToken());
233    }
234
235    /**
236     * Method to invoke {@link SDProtocol#deleteLineup(String,String)}.
237     *
238     * @param   lineup          The line-up to delete.
239     *
240     * @return  The {@link JsonNode}.
241     *
242     * @throws  Exception       If an exception is encountered invoking
243     *                          {@link SDProtocol#deleteLineup(String,String)}.
244     */
245    public JsonNode deleteLineup(String lineup) throws Exception {
246        return proxy().deleteLineup(getToken(), lineup);
247    }
248
249    /**
250     * Method to invoke
251     * {@link SDProtocol#getLineup(String,Boolean,String)}.
252     *
253     * @param   lineup          The line-up to get.
254     *
255     * @return  The {@link Lineup}.
256     *
257     * @throws  Exception       If an exception is encountered invoking
258     *                          {@link SDProtocol#getLineup(String,Boolean,String)}.
259     */
260    public Lineup getLineup(String lineup) throws Exception {
261        return proxy().getLineup(getToken(), true, lineup);
262    }
263
264    /**
265     * Method to invoke
266     * {@link SDProtocol#postLineup(String,HttpEntity)} to attempt to
267     * auto-map a discovered HDHR Prime line-up.
268     *
269     * @param   entity          The JSON entity retrieved from the HDHR
270     *                          Prime.
271     *
272     * @return  The {@link JsonNode}.
273     *
274     * @throws  Exception       If an exception is encountered invoking
275     *                          {@link SDProtocol#postLineup(String,HttpEntity)}.
276     *
277     * @see silicondust.HDHRPrimeClient#getLineup()
278     */
279    public JsonNode postLineup(HttpEntity entity) throws Exception {
280        return proxy().postLineup(getToken(), entity);
281    }
282
283    /**
284     * Method to invoke
285     * {@link SDProtocol#postLineup(String,String,HttpEntity)} to attempt to
286     * post a discovered HDHR Prime line-up to the server.
287     *
288     * @param   lineup          The name of the line-up.
289     * @param   entity          The JSON entity retrieved from the HDHR
290     *                          Prime.
291     *
292     * @return  The {@link JsonNode}.
293     *
294     * @throws  Exception       If an exception is encountered invoking
295     *                          {@link SDProtocol#postLineup(String,String,HttpEntity)}.
296     *
297     * @see silicondust.HDHRPrimeClient#getLineup()
298     */
299    public JsonNode postLineup(String lineup,
300                               HttpEntity entity) throws Exception {
301        return proxy().postLineup(getToken(), lineup, entity);
302    }
303
304    /**
305     * Method to invoke {@link SDProtocol#postPrograms(String,Collection)}.
306     *
307     * @param   ids             The {@link Collection} of program IDs,
308     *
309     * @return  The {@link List} of {@link Program}s.
310     *
311     * @throws  Exception       If an exception is encountered invoking
312     *                          {@link SDProtocol#postPrograms(String,Collection)}.
313     */
314    public List<Program> getPrograms(Collection<String> ids) throws Exception {
315        return proxy().postPrograms(getToken(), ids);
316    }
317
318    /**
319     * Method to invoke
320     * {@link SDProtocol#postProgramsDescription(String,Collection)}.
321     *
322     * @param   ids             The {@link Collection} of program IDs,
323     *
324     * @return  The {@link JsonNode}.
325     *
326     * @throws  Exception       If an exception is encountered invoking
327     *                          {@link SDProtocol#postProgramsDescription(String,Collection)}.
328     */
329    public JsonNode getProgramsDescription(Collection<String> ids) throws Exception {
330        return proxy().postProgramsDescription(getToken(), ids);
331    }
332
333    /**
334     * Method to invoke
335     * {@link SDProtocol#postProgramsMetadata(String,Collection)}.
336     *
337     * @param   ids             The {@link Collection} of program IDs,
338     *
339     * @return  The {@link JsonNode}.
340     *
341     * @throws  Exception       If an exception is encountered invoking
342     *                          {@link SDProtocol#postProgramsMetadata(String,Collection)}.
343     */
344    public JsonNode getProgramsMetadata(Collection<String> ids) throws Exception {
345        return proxy().postProgramsMetadata(getToken(), ids);
346    }
347
348    /**
349     * Method to invoke {@link SDProtocol#getProgramMetadata(String)}.
350     *
351     * @param   id              The root ID,
352     *
353     * @return  The {@link JsonNode}.
354     *
355     * @throws  Exception       If an exception is encountered invoking
356     *                          {@link SDProtocol#getProgramMetadata(String)}.
357     */
358    public JsonNode getProgramMetadata(String id) throws Exception {
359        return proxy().getProgramMetadata(id);
360    }
361
362    /**
363     * Method to invoke
364     * {@link SDProtocol#postSchedules(String,Collection)}.
365     *
366     * @param   ids             The {@link Collection} of station IDs,
367     *
368     * @return  The {@link List} of {@link SDProtocol.Schedules}.
369     *
370     * @throws  Exception       If an exception is encountered invoking
371     *                          {@link SDProtocol#postSchedules(String,Collection)}.
372     */
373    public List<SDProtocol.Schedules> getSchedules(Collection<?> ids) throws Exception {
374        return proxy().postSchedules(getToken(),
375                                     new PostSchedulesMap(ids).values());
376    }
377
378    /**
379     * {@link #getSchedules(Collection)} and
380     * {@link #getSchedulesMD5(Collection)} parameter: Use {@link #values()}
381     * as argument.
382     */
383    public static class PostSchedulesMap
384                        extends LinkedHashMap<Object,Map<String,Object>> {
385        private static final long serialVersionUID = 6106598221772498302L;
386
387        /**
388         * Sole constructor.
389         *
390         * @param   ids             The station IDs.
391         */
392        public PostSchedulesMap(Collection<?> ids) {
393            super();
394
395            for (Object key : ids) {
396                if (! containsKey(key)) {
397                    put(key, new LinkedHashMap<String,Object>());
398                }
399
400                get(key).put("stationID", key);
401            }
402        }
403    }
404
405    /**
406     * Method to invoke
407     * {@link SDProtocol#postSchedulesMD5(String,Collection)}.
408     *
409     * @param   ids             The {@link Collection} of station IDs,
410     *
411     * @return  The {@link JsonNode}.
412     *
413     * @throws  Exception       If an exception is encountered invoking
414     *                          {@link SDProtocol#postSchedulesMD5(String,Collection)}.
415     */
416    public JsonNode getSchedulesMD5(Collection<?> ids) throws Exception {
417        return proxy().postSchedulesMD5(getToken(),
418                                        new PostSchedulesMap(ids).values());
419    }
420
421    /**
422     * Method to invoke {@link SDProtocol#getCelebrityMetadata(String)}.
423     *
424     * @param   id              The root ID,
425     *
426     * @return  The {@link JsonNode}.
427     *
428     * @throws  Exception       If an exception is encountered invoking
429     *                          {@link SDProtocol#getCelebrityMetadata(String)}.
430     */
431    public JsonNode getCelebrityMetadata(String id) throws Exception {
432        return proxy().getCelebrityMetadata(id);
433    }
434
435    /**
436     * Method to invoke {@link SDProtocol#getImage(URI)} or
437     * {@link SDProtocol#getImage(String)} (as appropriate).
438     *
439     * @param   string          The image {@link URI}
440     *                          (as {@link String}).
441     *
442     * @return  The {@link HttpEntity}.
443     *
444     * @throws  Exception       If an exception is encountered invoking
445     *                          {@link SDProtocol#getImage(URI)} or
446     *                          {@link SDProtocol#getImage(String)}.
447     */
448    public HttpEntity getImage(String string) throws Exception {
449        URI uri = URI.create(string);
450        HttpResponse response =
451            uri.isAbsolute()
452                ? proxy().getImage(uri)
453                : proxy().getImage(string);
454
455        return response.getEntity();
456    }
457
458    @Override
459    public void process(HttpRequest request,
460                        HttpContext context) throws IOException {
461        super.process(request, context);
462    }
463
464    @Override
465    public void process(HttpResponse response,
466                        HttpContext context) throws IOException {
467        super.process(response, context);
468    }
469
470    @Override
471    public String toString() { return super.toString(); }
472
473    /**
474     * Static method to generate a 40-digit {@code SHA1} hash of a user
475     * password.
476     *
477     * @param   password        The user password.
478     *
479     * @return  The hash (as a 40-{@link Character} {@link String}.
480     */
481    public static String hash(String password) {
482        BigInteger hash = null;
483
484        try {
485            hash =
486                new BigInteger(1,
487                               MessageDigest.getInstance("SHA1")
488                               .digest(password.getBytes(UTF_8)));
489        } catch (Exception exception) {
490            throw new Error(exception);
491        }
492
493        return String.format("%040x", hash);
494    }
495}