org.thevortex.lighting.jinks.icons.WinkIconService.java Source code

Java tutorial

Introduction

Here is the source code for org.thevortex.lighting.jinks.icons.WinkIconService.java

Source

/*
 * Copyright (c) 2015 by the author(s).
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.thevortex.lighting.jinks.icons;

import java.io.IOException;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.node.ArrayNode;
import org.apache.http.HttpStatus;
import org.apache.http.StatusLine;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.thevortex.lighting.jinks.client.WinkClient;
import org.thevortex.lighting.jinks.client.WinkService;

/**
 * Get icons, because that's all that can be done. This service has a built-in cache that refreshes in a background
 * thread with a configurable period of time (default 12 hours). The updates start after the first request for any
 * icon(s).
 * <p>
 * If a cache type is specified, the image bytes will be fetched and cached as necessary/where possible.
 * </p>
 *
 * @author E. A. Graham Jr.
 */
public class WinkIconService implements WinkService<WinkIcon> {
    public static final String GET_URI = "/icons";

    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    // the cache
    private final Map<String, WinkIcon> iconCache = new ConcurrentHashMap<>();

    private final WinkClient client;

    private ScheduledExecutorService scheduler = Executors
            .newSingleThreadScheduledExecutor(r -> new Thread(r, "Wink Icon Cache Refresh"));
    private Duration timeout = Duration.ofHours(12);
    private ImageCacheType cacheType;

    public WinkIconService(WinkClient client) {
        this.client = client;
    }

    @Override
    public void enableNotifications(boolean enableNotify) {
        // ignored
    }

    /**
     * Set the timeout. If set to {@code null}, the cache is disabled.
     *
     * @param timeout time to wait before a cache checkCache is required
     */
    public void setCacheTimeout(Duration timeout) {
        this.timeout = timeout;
    }

    /**
     * Sets the cacheType of image to cache. If not set, images are not cached.
     *
     * @param type the cacheType
     */
    public void setImageCacheType(ImageCacheType type) {
        this.cacheType = type;
    }

    @Override
    public void close() {
    }

    /**
     * {@inheritDoc}
     * <p>
     * Reads from cache, but refreshes the cache on startup and starts the background scheduled refresh.
     * </p>
     */
    @Override
    public synchronized SortedSet<WinkIcon> getAll() throws IOException {
        checkCache();
        return new TreeSet<>(iconCache.values());
    }

    /**
     * {@inheritDoc}
     * <p>
     * Reads from cache, but refreshes the cache on startup and starts the background scheduled refresh.
     * </p>
     */
    @Override
    public WinkIcon get(String id) throws IOException {
        checkCache();

        WinkIcon icon = iconCache.get(id);
        if (icon != null && icon.getImageBytes() == null && cacheType != null) {
            String url = cacheType.getSelector(icon);
            icon.setImageBytes(getBytes(url));
        }
        return icon;
    }

    @Override
    public void addServiceChangeListener(ServiceChangeListener<WinkIcon> listener) {
        // does nothing
    }

    @Override
    public void removeServiceChangeListener(ServiceChangeListener<WinkIcon> listener) {
        // does nothing
    }

    /**
     * Check to see if the cache needs refreshing.
     *
     * @throws IOException can't get it
     */
    private synchronized void checkCache() throws IOException {
        // if the cache exists, we're done
        if (!iconCache.isEmpty())
            return;

        // get it and start the background updates
        updateCache();

        Runnable command = () -> {
            try {
                WinkIconService.this.updateCache();

                // get bytes
                logger.info("Fetching bytes for empty values.");
                for (WinkIcon icon : iconCache.values()) {
                    if (!hasBytes(icon.getImageBytes())) {
                        String url = cacheType.getSelector(icon);
                        icon.setImageBytes(WinkIconService.this.getBytes(url));
                    }
                }
            } catch (IOException e) {
                logger.error("Error updating icon cache", e);
            }
        };
        scheduler.scheduleAtFixedRate(command, 1, timeout.toMinutes(), TimeUnit.MINUTES);
    }

    /**
     * All the data.
     *
     * @return {@code true} if the cache updated
     * @throws IOException barf
     */
    private boolean updateCache() throws IOException {
        logger.info("Cache refreshing");
        Map<String, WinkIcon> temp = new HashMap<>();

        JsonNode node = client.doGet(GET_URI);
        if (!node.has("data"))
            return false;
        ArrayNode jsonList = (ArrayNode) node.get("data");
        for (JsonNode jsonNode : jsonList) {
            WinkIcon icon = WinkClient.MAPPER.convertValue(jsonNode, WinkIcon.class);
            String id = icon.getId();
            temp.put(id, icon);

            // if the old one exists and the images haven't changed, keep bytes
            if (iconCache.containsKey(id)) {
                WinkIcon original = iconCache.get(id);
                if (!original.stateChanged(icon)) {
                    byte[] imageBytes = original.getImageBytes();
                    if (hasBytes(imageBytes))
                        icon.setImageBytes(imageBytes);
                }
            }
        }

        iconCache.clear();
        iconCache.putAll(temp);
        return true;
    }

    private boolean hasBytes(byte[] imageBytes) {
        return imageBytes != null && imageBytes.length > 0;
    }

    /**
     * Fetch the bytes of the requested image type.
     *
     * @param url the location of the image
     * @return the fetched bytes or a 0-length array as a place-holder
     */
    private synchronized byte[] getBytes(String url) {
        CloseableHttpClient httpClient = client.getClient();
        try {
            HttpGet get = new HttpGet(url);
            try (CloseableHttpResponse response = httpClient.execute(get)) {
                StatusLine statusLine = response.getStatusLine();
                int statusCode = statusLine.getStatusCode();
                if (statusCode == HttpStatus.SC_OK) {
                    return EntityUtils.toByteArray(response.getEntity());
                }
                logger.error("Response for Icon fetch was {}: {}", statusCode, statusLine.getReasonPhrase());
            }
        } catch (IOException e) {
            logger.error("Cannot fetch Icon at {}", url, e);
        }
        return new byte[0];
    }
}