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