org.openhab.io.hueemulation.internal.RESTApi.java Source code

Java tutorial

Introduction

Here is the source code for org.openhab.io.hueemulation.internal.RESTApi.java

Source

/**
 * Copyright (c) 2010-2019 Contributors to the openHAB project
 *
 * See the NOTICE file(s) distributed with this work for additional
 * information.
 *
 * This program and the accompanying materials are made available under the
 * terms of the Eclipse Public License 2.0 which is available at
 * http://www.eclipse.org/legal/epl-2.0
 *
 * SPDX-License-Identifier: EPL-2.0
 */
package org.openhab.io.hueemulation.internal;

import java.io.IOException;
import java.io.Writer;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.time.LocalDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.UUID;

import org.eclipse.jdt.annotation.NonNullByDefault;
import org.eclipse.jdt.annotation.Nullable;
import org.eclipse.smarthome.core.events.EventPublisher;
import org.eclipse.smarthome.core.items.events.ItemEventFactory;
import org.eclipse.smarthome.core.types.Command;
import org.openhab.io.hueemulation.internal.dto.HueDataStore;
import org.openhab.io.hueemulation.internal.dto.HueDevice;
import org.openhab.io.hueemulation.internal.dto.HueNewLights;
import org.openhab.io.hueemulation.internal.dto.HueStateChange;
import org.openhab.io.hueemulation.internal.dto.HueUnauthorizedConfig;
import org.openhab.io.hueemulation.internal.dto.changerequest.HueChangeRequest;
import org.openhab.io.hueemulation.internal.dto.changerequest.HueCreateUser;
import org.openhab.io.hueemulation.internal.dto.response.HueResponse;
import org.openhab.io.hueemulation.internal.dto.response.HueResponse.HueErrorMessage;
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessCreateGroup;
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseCreateUser;
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseStartSearchLights;
import org.openhab.io.hueemulation.internal.dto.response.HueSuccessResponseStateChanged;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonWriter;

/**
 * Handles all REST API Requests
 *
 * @author David Graeff - Initial contribution
 */
@NonNullByDefault
public class RESTApi {
    public static final String PATH = "/api";
    private final Logger logger = LoggerFactory.getLogger(HueEmulationService.class);
    private final HueDataStore ds;
    private final Gson gson;
    private final UserManagement userManagement;
    private final ConfigManagement configManagement;
    private @NonNullByDefault({}) EventPublisher eventPublisher;

    public static enum HttpMethod {
        GET, POST, PUT, DELETE
    }

    public RESTApi(HueDataStore ds, UserManagement userManagement, ConfigManagement configManagement, Gson gson) {
        this.ds = ds;
        this.userManagement = userManagement;
        this.configManagement = configManagement;
        this.gson = gson;
    }

    public void setEventPublisher(@Nullable EventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    /**
     * Cuts of the first part of a path and returns the remaining one.
     */
    private Path remaining(Path path) {
        if (path.getNameCount() > 1) {
            return path.subpath(1, path.getNameCount());
        } else {
            return Paths.get("/");
        }
    }

    /**
     * Handles /api and forwards any deeper path
     *
     * @param isDebug
     */
    @SuppressWarnings("null")
    public int handle(HttpMethod method, String body, Writer out, Path path, boolean isDebug)
            throws IOException, JsonParseException {
        if (!"api".equals(path.getName(0).toString())) {
            return 404;
        }

        if (path.getNameCount() == 1) { // request for API key
            if (method != HttpMethod.POST) {
                return 405;
            }
            if (!ds.config.linkbutton) {
                return 10403;
            }

            final HueCreateUser userRequest;
            userRequest = gson.fromJson(body, HueCreateUser.class);
            if (userRequest.devicetype == null || userRequest.devicetype.isEmpty()) {
                throw new JsonParseException("devicetype not given");
            }

            String apiKey = userRequest.username;
            if (apiKey == null || apiKey.length() == 0) {
                apiKey = UUID.randomUUID().toString();
            }
            userManagement.addUser(apiKey, userRequest.devicetype);

            try (JsonWriter writer = new JsonWriter(out)) {
                HueSuccessResponseCreateUser h = new HueSuccessResponseCreateUser(apiKey);
                gson.toJson(Collections.singleton(new HueResponse(h)), new TypeToken<List<?>>() {
                }.getType(), writer);
            }
            return 200;
        }

        updateDataStore();

        Path userPath = remaining(path);

        return handleUser(method, body, out, userPath.getName(0).toString(), remaining(userPath), path, isDebug);
    }

    /**
     * Handles /api/config and /api/{user-name} and forwards any deeper path
     */
    public int handleUser(HttpMethod method, String body, Writer out, String userName, Path remainingPath,
            Path fullURI, boolean isDebug) throws IOException, JsonParseException {

        if ("config".equals(userName)) { // Reduced config
            try (JsonWriter writer = new JsonWriter(out)) {
                gson.toJson(ds.config, new TypeToken<HueUnauthorizedConfig>() {
                }.getType(), writer);
            }
            return 200;
        }

        if (!userManagement.authorizeUser(userName)) {
            if (ds.config.linkbutton && ds.config.createNewUserOnEveryEndpoint) {
                userManagement.addUser(userName, "Formerly authorized device");
            } else {
                return 403;
            }
        }

        if (remainingPath.getNameCount() == 0) { /** /api/{username} */
            switch (method) {
            case GET:
                out.write(gson.toJson(ds));
                return 200;
            default:
                return 405;
            }

        }

        String function = remainingPath.getName(0).toString();

        switch (function) {
        case "lights":
            return handleLights(method, body, out, remaining(remainingPath), fullURI, isDebug);
        case "groups":
            return handleGroups(method, body, out, remaining(remainingPath));
        case "config":
            return handleConfig(method, body, out, remaining(remainingPath), userName);
        default:
            return 404;
        }
    }

    /**
     * Handles /api/{user-name}/config and /api/{user-name}/config/whitelist
     * The own whitelisted user can remove itself with a DELETE
     */
    public int handleConfig(HttpMethod method, String body, Writer out, Path remainingPath, String authorizedUser)
            throws IOException, JsonParseException {
        if (remainingPath.getNameCount() == 0) {
            switch (method) {
            case GET:
                out.write(gson.toJson(ds.config));
                return 200;
            case PUT:
                final HueChangeRequest changes;
                changes = gson.fromJson(body, HueChangeRequest.class);
                if (changes.devicename != null) {
                    ds.config.devicename = changes.devicename;
                }
                if (changes.dhcp != null) {
                    ds.config.dhcp = changes.dhcp;
                }
                if (changes.linkbutton != null) {
                    ds.config.linkbutton = changes.linkbutton;
                    configManagement.checkPairingTimeout();
                }
                configManagement.writeToFile();
                return 200;
            default:
                return 405;
            }
        } else if (remainingPath.getNameCount() >= 1 && "whitelist".equals(remainingPath.getName(0).toString())) {
            return handleConfigWhitelist(method, out, remaining(remainingPath), authorizedUser);
        } else {
            return 404;
        }
    }

    public int handleConfigWhitelist(HttpMethod method, Writer out, Path remainingPath, String authorizedUser)
            throws IOException {
        switch (remainingPath.getNameCount()) {
        case 0:
            switch (method) {
            case GET:
                out.write(gson.toJson(ds.config.whitelist));
                return 200;
            default:
                return 405;
            }
        case 1:
            String username = remainingPath.getName(0).toString();
            switch (method) {
            case GET:
                ds.config.whitelist.get(username);
                out.write(gson.toJson(ds.config.whitelist));
                return 200;
            case DELETE:
                // Only own user can be removed
                if (username.equals(authorizedUser)) {
                    userManagement.removeUser(authorizedUser);
                    return 200;
                } else {
                    return 403;
                }
            default:
                return 405;
            }
        default:
            return 405;
        }
    }

    @SuppressWarnings({ "null", "unused" })
    public int handleLights(HttpMethod method, String body, Writer out, Path remainingPath, Path fullURI,
            boolean isDebug) throws IOException, JsonParseException {
        /** /api/{username}/lights */
        if (remainingPath.getNameCount() == 0) {
            switch (method) {
            case GET:
                if (isDebug) {
                    out.write("Exposed lights:\n\n");
                    for (HueDevice hueDevice : ds.lights.values()) {
                        out.write(hueDevice.toString());
                        out.write("\n");
                    }
                } else {
                    ds.lights.values().forEach(v -> v.updateState());
                    out.write(gson.toJson(ds.lights));
                }
                return 200;
            case POST:
                try (JsonWriter writer = new JsonWriter(out)) {
                    List<HueResponse> responses = new ArrayList<>();
                    responses.add(new HueResponse(new HueSuccessResponseStartSearchLights()));
                    gson.toJson(responses, new TypeToken<List<?>>() {
                    }.getType(), writer);
                }
                return 200;
            default:
                return 405;
            }
        }

        String id = remainingPath.getName(0).toString();

        /** /api/{username}/lights/new */
        if ("new".equals(id)) {
            switch (method) {
            case GET:
                out.write(gson.toJson(new HueNewLights()));
                return 200;
            default:
                return 405;
            }
        }

        final int hueID;
        try {
            hueID = new Integer(id);
        } catch (NumberFormatException e) {
            return 404;
        }

        HueDevice hueDevice = ds.lights.get(hueID);
        if (hueDevice == null) {
            return 404;
        }

        /** /api/{username}/lights/{id} */
        if (remainingPath.getNameCount() == 1) {
            hueDevice.updateState();
            out.write(gson.toJson(hueDevice));
            return 200;
        }

        if (remainingPath.getNameCount() == 2) {
            switch (method) {
            case PUT:
                return handleLightChangeState(fullURI, method, body, out, hueID, hueDevice);
            default:
                return 405;
            }
        }

        return 404;
    }

    @SuppressWarnings({ "null", "unused" })
    public int handleGroups(HttpMethod method, String body, Writer out, Path remainingPath) throws IOException {
        /** /api/{username}/groups */
        if (remainingPath.getNameCount() == 0) {
            switch (method) {
            case GET:
                out.write(gson.toJson(ds.groups));
                return 200;
            case POST:
                int hueid = ds.generateNextGroupHueID();
                try (JsonWriter writer = new JsonWriter(out)) {
                    List<HueResponse> responses = new ArrayList<>();
                    responses.add(new HueResponse(new HueSuccessCreateGroup(hueid)));
                    gson.toJson(responses, new TypeToken<List<?>>() {
                    }.getType(), writer);
                }
                return 200;
            default:
                return 405;
            }
        }

        String id = remainingPath.getName(0).toString();

        final int hueID;
        try {
            hueID = new Integer(id);
        } catch (NumberFormatException e) {
            return 404;
        }

        /** /api/{username}/groups/{id} */
        if (remainingPath.getNameCount() == 1) {
            Object value = ds.groups.get(hueID);
            if (value == null) {
                return 404;
            } else {
                out.write(gson.toJson(value));
                return 200;
            }
        }
        return 404;
    }

    /**
     * Hue API call to set the state of a light.
     * Enpoint: /api/{username}/lights/{id}/state
     */
    @SuppressWarnings({ "null", "unused" })
    private int handleLightChangeState(Path fullURI, HttpMethod method, String body, Writer out, int hueID,
            HueDevice hueDevice) throws IOException, JsonParseException {
        HueStateChange state = gson.fromJson(body, HueStateChange.class);
        if (state == null) {
            throw new JsonParseException("No state change data received!");
        }

        // logger.debug("Received state change: {}", gson.toJson(state));

        // Apply new state and collect success, error items
        Map<String, Object> successApplied = new TreeMap<>();
        List<String> errorApplied = new ArrayList<>();
        Command command = hueDevice.applyState(state, successApplied, errorApplied);

        // If a command could be created, post it to the framework now
        if (command != null) {
            logger.debug("sending {} to {}", command, hueDevice.item.getName());
            eventPublisher
                    .post(ItemEventFactory.createCommandEvent(hueDevice.item.getName(), command, "hueemulation"));
        }

        // Generate the response. The response consists of a list with an entry each for all
        // submitted change requests. If for example "on" and "bri" was send, 2 entries in the response are
        // expected.
        Path contextPath = fullURI.subpath(2, fullURI.getNameCount() - 1);
        List<HueResponse> responses = new ArrayList<>();
        successApplied.forEach((t, v) -> {
            responses
                    .add(new HueResponse(new HueSuccessResponseStateChanged(contextPath.resolve(t).toString(), v)));
        });
        errorApplied.forEach(v -> {
            responses.add(new HueResponse(new HueErrorMessage(HueResponse.NOT_AVAILABLE,
                    contextPath.resolve(v).toString(), "Could not set")));
        });

        try (JsonWriter writer = new JsonWriter(out)) {
            gson.toJson(responses, new TypeToken<List<?>>() {
            }.getType(), writer);
        }
        return 200;
    }

    /**
     * Update changing parameters of the data store like the time.
     */
    public void updateDataStore() {
        ds.config.UTC = LocalDateTime.now(ZoneOffset.UTC).format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
        ds.config.localtime = LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME);
    }
}