/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for
 * license information.
 */
package com.microsoft.azure.spring.cloundfoundry.environment;

import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import lombok.Getter;
import lombok.Setter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.MapPropertySource;
import org.springframework.core.env.MutablePropertySources;
import org.springframework.core.env.PropertySource;

import java.io.Serializable;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

@Getter
@Setter
public class VcapResult implements Serializable {

    private static final Logger LOGGER = LoggerFactory.getLogger(VcapResult.class);

    private static final String AZURE_SERVICE_BUS_DOMAIN = "servicebus.windows.net";

    private static final String PROPERTY_SOURCE_NAME = "defaultProperties";
    private static final String RESULT = "result";
    private static final String CONNECTION_STRING = "connectionString";
    private static final String URI = "uri";
    private static final String KEY = "key";
    private static final String DATABASE = "database";
    private static final long serialVersionUID = -4825963001214199795L;

    private final boolean logFlag;

    public VcapResult(ConfigurableEnvironment environment, VcapPojo[] pojos, boolean logFlag) {
        this.logFlag = logFlag;

        populateProperties(environment, pojos);
    }

    /**
     * Populates default properties during @EnvironmentPostProcessor processing.
     * <p>
     * Note that this class gets invoked before Spring creates the logging
     * subsystem, so we just use System.out.println instead.
     */
    @SuppressFBWarnings("UWF_FIELD_NOT_INITIALIZED_IN_CONSTRUCTOR")
    private void populateProperties(ConfigurableEnvironment environment, VcapPojo[] pojos) {
        final Map<String, Object> map = new HashMap<>();
        populateDefaultStorageProperties(map,
                findPojoForServiceType(VcapServiceType.AZURE_STORAGE, pojos));
        populateDefaultServiceBusProperties(map,
                findPojoForServiceType(VcapServiceType.AZURE_SERVICEBUS, pojos));
        populateDefaultDocumentDBProperties(map,
                findPojoForServiceType(VcapServiceType.AZURE_DOCUMENTDB, pojos));
        addOrReplace(environment.getPropertySources(), map);
    }

    private VcapPojo findPojoForServiceType(VcapServiceType serviceType, VcapPojo[] pojos) {
        if (serviceType == null) {
            log("VcapResult.findPojoForServiceType: ServiceType is null, no service found.");
            return null;
        }

        VcapPojo pojo = null;

        switch (findCountByServiceType(serviceType, pojos)) {
            case 0:
                log("VcapResult.findPojoForServiceType: No services of type "
                        + serviceType.toString() + " found.");
                break;
            case 1:
                log("VcapResult.findPojoForServiceType: One services of type "
                        + serviceType.toString() + " found.");
                pojo = findByServiceType(serviceType, pojos);
                if (pojo != null) {
                    log("VcapResult.findPojoForServiceType: Found the matching pojo");
                }
                break;
            default:
                log("VcapResult.findPojoForServiceType: More than one service of type "
                        + serviceType.toString()
                        + " found, cannot autoconfigure service, must use factory instead.");
                break;
        }
        return pojo;
    }

    private int findCountByServiceType(VcapServiceType serviceType, VcapPojo[] pojos) {
        int result = 0;

        if (serviceType != null) {
            for (int i = 0; i < pojos.length; i++) {
                final VcapPojo pojo = pojos[i];
                if (serviceType.toString().equals(pojo.getServiceBrokerName())) {
                    result++;
                }
            }
        }

        return result;
    }

    private void populateDefaultStorageProperties(Map<String, Object> map,
                                                  VcapPojo pojo) {
        log("VcapResult.populateDefaultStorageProperties " + pojo);
        map.put(Constants.NAMESPACE_STORAGE + "." + RESULT, this);
        if (pojo != null) {
            map.put(Constants.NAMESPACE_STORAGE + "." + CONNECTION_STRING,
                    buildStorageConnectionString(pojo));
            log("VcapResult.populateDefaultStorageProperties: Updated Storage properties");
        }
    }

    private String buildStorageConnectionString(VcapPojo pojo) {
        final String storageConnectionString =
                "DefaultEndpointsProtocol=http;"
                        +
                        "AccountName="
                        + pojo.getServiceConfig().getCredentials().get(
                        Constants.STORAGE_ACCOUNT_NAME)
                        + ";"
                        +
                        "AccountKey="
                        + pojo.getServiceConfig().getCredentials().get(
                        Constants.PRIMARY_ACCESS_KEY);
        log("storageConnectionString = " + storageConnectionString);
        return storageConnectionString;
    }

    private void populateDefaultServiceBusProperties(Map<String, Object> map,
                                                     VcapPojo pojo) {
        log("VcapResult.populateDefaultServiceBusProperties " + pojo);
        map.put(Constants.NAMESPACE_SERVICE_BUS + "." + RESULT, this);
        if (pojo != null) {
            map.put(Constants.NAMESPACE_SERVICE_BUS + "." + CONNECTION_STRING,
                    buildServiceBusConnectString(pojo));
            log("VcapResult.populateDefaultServiceBusProperties: Updated Service Bus properties");
        }
    }

    private String buildServiceBusConnectString(VcapPojo pojo) {
        final String connectionString =
                "Endpoint=sb://"
                        + pojo.getServiceConfig().getCredentials().get(Constants.NAMESPACE_NAME)
                        + "."
                        + AZURE_SERVICE_BUS_DOMAIN
                        + "/;"
                        + "SharedAccessKeyName="
                        + pojo.getServiceConfig().getCredentials().get(
                        Constants.SHARED_ACCESS_NAME)
                        + ";"
                        + "SharedAccessKey="
                        + pojo.getServiceConfig().getCredentials().get(
                        Constants.SHARED_ACCESS_KEY_VALUE);
        log("connectionString name = " + connectionString);
        return connectionString;
    }

    private void populateDefaultDocumentDBProperties(Map<String, Object> map,
                                                     VcapPojo pojo) {
        log("VcapResult.populateDefaultDocumentDBProperties " + pojo);
        map.put(Constants.NAMESPACE_DOCUMENTDB + "." + RESULT, this);
        if (pojo != null) {
            map.put(Constants.NAMESPACE_DOCUMENTDB + "." + URI, pojo
                    .getServiceConfig().getCredentials().get(Constants.HOST_ENDPOINT));
            map.put(Constants.NAMESPACE_DOCUMENTDB + "." + KEY, pojo
                    .getServiceConfig().getCredentials().get(Constants.MASTER_KEY));
            map.put(Constants.NAMESPACE_DOCUMENTDB + "." + DATABASE, pojo
                    .getServiceConfig().getCredentials().get(Constants.DATABASE_ID));
            log("VcapResult.populateDefaultDocumentDBProperties: Updated DocumentDB properties");
        }
    }

    private VcapPojo findByServiceType(VcapServiceType serviceType, VcapPojo[] pojos) {
        VcapPojo result = null;

        if (serviceType != null) {
            for (int i = 0; i < pojos.length; i++) {
                final VcapPojo pojo = pojos[i];
                if (serviceType.toString().equals(pojo.getServiceBrokerName())) {
                    result = pojo;
                    break;
                }
            }
        }

        return result;
    }

    @SuppressFBWarnings("WMI_WRONG_MAP_ITERATOR")
    private void addOrReplace(MutablePropertySources propertySources,
                              Map<String, Object> map) {
        MapPropertySource target = null;
        if (propertySources.contains(PROPERTY_SOURCE_NAME)) {
            final PropertySource<?> source = propertySources
                    .get(PROPERTY_SOURCE_NAME);
            if (source instanceof MapPropertySource) {
                target = (MapPropertySource) source;
                for (final Entry<String, Object> entry : map.entrySet()) {
                    if (!target.containsProperty(entry.getKey())) {
                        target.getSource().put(entry.getKey(), map.get(entry.getKey()));
                    }
                }
            }
        }
        if (target == null) {
            target = new MapPropertySource(PROPERTY_SOURCE_NAME, map);
        }
        if (!propertySources.contains(PROPERTY_SOURCE_NAME)) {
            propertySources.addLast(target);
        }
    }

    private void log(String msg) {
        if (logFlag) {
            LOGGER.info(msg);
        }
    }

}


/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for
 * license information.
 */
package com.microsoft.azure.spring.autoconfigure.aad;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.microsoft.aad.adal4j.AuthenticationContext;
import com.microsoft.aad.adal4j.AuthenticationResult;
import com.microsoft.aad.adal4j.ClientCredential;
import com.microsoft.aad.adal4j.UserAssertion;
import com.microsoft.azure.spring.autoconfigure.aad.AADAuthenticationProperties.UserGroupProperties;
import com.nimbusds.oauth2.sdk.http.HTTPResponse;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;

import javax.naming.ServiceUnavailableException;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;

public class AzureADGraphClient {
    private static final SimpleGrantedAuthority DEFAULT_AUTHORITY = new SimpleGrantedAuthority("ROLE_USER");
    private static final String DEFAULT_ROLE_PREFIX = "ROLE_";
    private static final String REQUEST_ID_SUFFIX = "aadfeed5";

    private final String clientId;
    private final String clientSecret;
    private final ServiceEndpoints serviceEndpoints;
    private final AADAuthenticationProperties aadAuthenticationProperties;

    public AzureADGraphClient(ClientCredential clientCredential, AADAuthenticationProperties aadAuthProps,
                              ServiceEndpointsProperties serviceEndpointsProps) {
        this.clientId = clientCredential.getClientId();
        this.clientSecret = clientCredential.getClientSecret();
        this.aadAuthenticationProperties = aadAuthProps;
        this.serviceEndpoints = serviceEndpointsProps.getServiceEndpoints(aadAuthProps.getEnvironment());
    }

    private String getUserMembershipsV1(String accessToken) throws IOException {
        final URL url = new URL(serviceEndpoints.getAadMembershipRestUri());

        final HttpURLConnection conn = (HttpURLConnection) url.openConnection();
        // Set the appropriate header fields in the request header.
        conn.setRequestProperty("api-version", "1.6");
        conn.setRequestProperty("Authorization", accessToken);
        conn.setRequestProperty("Accept", "application/json;odata=minimalmetadata");
        final String responseInJson = getResponseStringFromConn(conn);
        final int responseCode = conn.getResponseCode();
        if (responseCode == HTTPResponse.SC_OK) {
            return responseInJson;
        } else {
            throw new IllegalStateException("Response is not " + HTTPResponse.SC_OK +
                    ", response json: " + responseInJson);
        }
    }

    private static String getResponseStringFromConn(HttpURLConnection conn) throws IOException {

        try (BufferedReader reader = new BufferedReader(
                new InputStreamReader(conn.getInputStream(), StandardCharsets.UTF_8))) {
            final StringBuilder stringBuffer = new StringBuilder();
            String line = "";
            while ((line = reader.readLine()) != null) {
                stringBuffer.append(line);
            }
            return stringBuffer.toString();
        }
    }

    public List<UserGroup> getGroups(String graphApiToken) throws IOException {
        return loadUserGroups(graphApiToken);
    }

    private List<UserGroup> loadUserGroups(String graphApiToken) throws IOException {
        final String responseInJson = getUserMembershipsV1(graphApiToken);
        final List<UserGroup> lUserGroups = new ArrayList<>();
        final ObjectMapper objectMapper = JacksonObjectMapperFactory.getInstance();
        final JsonNode rootNode = objectMapper.readValue(responseInJson, JsonNode.class);
        final JsonNode valuesNode = rootNode.get("value");

        if (valuesNode != null) {

            lUserGroups
                    .addAll(StreamSupport.stream(valuesNode.spliterator(), false).filter(this::isMatchingUserGroupKey)
                            .map(node -> {
                                final String objectID = node.
                                        get(aadAuthenticationProperties.getUserGroup().getObjectIDKey()).asText();
                                final String displayName = node.get("displayName").asText();
                                return new UserGroup(objectID, displayName);
                            }).collect(Collectors.toList()));

        }

        return lUserGroups;
    }

    /**
     * Checks that the JSON Node is a valid User Group to extract User Groups from
     *
     * @param node - json node to look for a key/value to equate against the {@link UserGroupProperties}
     *
     * @return true if the json node contains the correct key, and expected value to identify a user group.
     */
    private boolean isMatchingUserGroupKey(final JsonNode node) {
        return node.get(aadAuthenticationProperties.getUserGroup().getKey()).asText()
                .equals(aadAuthenticationProperties.getUserGroup().getValue());
    }

    public Set<GrantedAuthority> getGrantedAuthorities(String graphApiToken) throws IOException {
        // Fetch the authority information from the protected resource using accessToken
        final List<UserGroup> groups = getGroups(graphApiToken);

        // Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
        return convertGroupsToGrantedAuthorities(groups);
    }

    /**
     * Converts UserGroup list to Set of GrantedAutorities
     * @param groups
     * @return
     */
    public Set<GrantedAuthority> convertGroupsToGrantedAuthorities(final List<UserGroup> groups) {
        // Map the authority information to one or more GrantedAuthority's and add it to mappedAuthorities
        final Set<GrantedAuthority> mappedAuthorities = groups.stream().filter(this::isValidUserGroupToGrantAuthority)
                .map(userGroup -> new SimpleGrantedAuthority(DEFAULT_ROLE_PREFIX + userGroup.getDisplayName()))
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (mappedAuthorities.isEmpty()) {
            mappedAuthorities.add(DEFAULT_AUTHORITY);
        }

        return mappedAuthorities;
    }

    /**
     * Determines if this is a valid {@link UserGroup} to build to a GrantedAuthority.
     * <p>
     * If the {@link UserGroupProperties#getAllowedGroups()} or the {@link
     * AADAuthenticationProperties#getActiveDirectoryGroups()} contains the {@link UserGroup#getDisplayName()} return
     * true.
     *
     * @param group - User Group to check if valid to grant an authority to.
     *
     * @return true if either of the allowed-groups or active-directory-groups contains the UserGroup display name
     */
    private boolean isValidUserGroupToGrantAuthority(final UserGroup group) {
        return aadAuthenticationProperties.getUserGroup().getAllowedGroups().contains(group.getDisplayName())
                || aadAuthenticationProperties.getActiveDirectoryGroups().contains(group.getDisplayName());
    }

    public AuthenticationResult acquireTokenForGraphApi(String idToken, String tenantId)
            throws MalformedURLException, ServiceUnavailableException, InterruptedException, ExecutionException {

        final ClientCredential credential = new ClientCredential(clientId, clientSecret);
        final UserAssertion assertion = new UserAssertion(idToken);

        AuthenticationResult result = null;
        ExecutorService service = null;
        try {
            service = Executors.newFixedThreadPool(1);
            final AuthenticationContext context = new AuthenticationContext(
                    serviceEndpoints.getAadSigninUri() + tenantId + "/", true, service);
            context.setCorrelationId(getCorrelationId());
            final Future<AuthenticationResult> future = context
                    .acquireToken(serviceEndpoints.getAadGraphApiUri(), assertion, credential, null);
            result = future.get();
        } finally {
            if (service != null) {
                service.shutdown();
            }
        }

        if (result == null) {
            throw new ServiceUnavailableException("unable to acquire on-behalf-of token for client " + clientId);
        }
        return result;
    }

    private static String getCorrelationId() {
        final String uuid = UUID.randomUUID().toString();
        return uuid.substring(0, uuid.length() - REQUEST_ID_SUFFIX.length()) + REQUEST_ID_SUFFIX;
    }
}


/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for
 * license information.
 */

package com.microsoft.azure.keyvault.spring;

import com.microsoft.azure.PagedList;
import com.microsoft.azure.keyvault.KeyVaultClient;
import com.microsoft.azure.keyvault.models.SecretBundle;
import com.microsoft.azure.keyvault.models.SecretItem;
import org.springframework.lang.NonNull;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;

import java.util.Collections;
import java.util.Locale;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class KeyVaultOperation {
    private final long cacheRefreshIntervalInMs;
    private final Object refreshLock = new Object();
    private final KeyVaultClient keyVaultClient;
    private final String vaultUri;
    private ConcurrentHashMap<String, Object> propertyNamesHashMap = new ConcurrentHashMap<>();
    private final AtomicLong lastUpdateTime = new AtomicLong();
    private final ReadWriteLock rwLock = new ReentrantReadWriteLock();

    public KeyVaultOperation(final KeyVaultClient keyVaultClient, final String vaultUri) {
        this(keyVaultClient, vaultUri, Constants.DEFAULT_REFRESH_INTERVAL_MS);
    }

    public KeyVaultOperation(final KeyVaultClient keyVaultClient, String vaultUri, final long refreshInterval) {
        this.cacheRefreshIntervalInMs = refreshInterval;
        this.keyVaultClient = keyVaultClient;
        // TODO(pan): need to validate why last '/' need to be truncated.
        this.vaultUri = StringUtils.trimTrailingCharacter(vaultUri.trim(), '/');

        fillSecretsHashMap();
    }

    public String[] list() {
        try {
            this.rwLock.readLock().lock();
            return Collections.list(propertyNamesHashMap.keys()).toArray(new String[propertyNamesHashMap.size()]);
        } finally {
            this.rwLock.readLock().unlock();
        }
    }

    private String getKeyvaultSecretName(@NonNull String property) {
        if (property.matches("[a-z0-9A-Z-]+")) {
            return property.toLowerCase(Locale.US);
        } else if (property.matches("[A-Z0-9_]+")) {
            return property.toLowerCase(Locale.US).replaceAll("_", "-");
        } else {
            return property.toLowerCase(Locale.US)
                    .replaceAll("-", "")     // my-project -> myproject
                    .replaceAll("_", "")     // my_project -> myproject
                    .replaceAll("\\.", "-"); // acme.myproject -> acme-myproject
        }
    }

    /**
     * For convention we need to support all relaxed binding format from spring, these may include:
     *

     *
Spring relaxed binding names

     *
acme.my-project.person.first-name

     *
acme.myProject.person.firstName

     *
acme.my_project.person.first_name

     *
ACME_MYPROJECT_PERSON_FIRSTNAME

     *

     * But azure keyvault only allows ^[0-9a-zA-Z-]+$ and case insensitive, so there must be some conversion
     * between spring names and azure keyvault names.
     * For example, the 4 properties stated above should be convert to acme-myproject-person-firstname in keyvault.
     *
     * @param property of secret instance.
     * @return the value of secret with given name or null.
     */
    public String get(final String property) {
        Assert.hasText(property, "property should contain text.");
        final String secretName = getKeyvaultSecretName(property);

        // refresh periodically
        if (System.currentTimeMillis() - this.lastUpdateTime.get() > this.cacheRefreshIntervalInMs) {
            synchronized (this.refreshLock) {
                if (System.currentTimeMillis() - this.lastUpdateTime.get() > this.cacheRefreshIntervalInMs) {
                    this.lastUpdateTime.set(System.currentTimeMillis());
                    fillSecretsHashMap();
                }
            }
        }

        if (this.propertyNamesHashMap.containsKey(secretName)) {
            final SecretBundle secretBundle = this.keyVaultClient.getSecret(this.vaultUri, secretName);
            return secretBundle.value();
        } else {
            return null;
        }
    }

    private void fillSecretsHashMap() {
        try {
            this.rwLock.writeLock().lock();
            this.propertyNamesHashMap.clear();

            final PagedList<SecretItem> secrets = this.keyVaultClient.listSecrets(this.vaultUri);
            secrets.loadAll();

            secrets.forEach(s -> {
                final String secretName = s.id().replace(vaultUri + "/secrets/", "").toLowerCase(Locale.US);
                propertyNamesHashMap.putIfAbsent(secretName, s.id());
                propertyNamesHashMap.putIfAbsent(secretName.replaceAll("-", "."), s.id());
            });

            this.lastUpdateTime.set(System.currentTimeMillis());
        } finally {
            this.rwLock.writeLock().unlock();
        }
    }
}


/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for
 * license information.
 */
package com.microsoft.azure.spring.autoconfigure.aad;

import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.Assert;

import java.util.HashMap;
import java.util.Map;

@Configuration
@ConfigurationProperties("azure.service")
public class ServiceEndpointsProperties {
    private Map<String, ServiceEndpoints> endpoints = new HashMap<>();

    public Map<String, ServiceEndpoints> getEndpoints() {
        return endpoints;
    }

    /**
     * Get ServiceEndpoints data for the given environment.
     *
     * @param environment the environment of the cloud service
     * @return The ServiceEndpoints data for the given azure service environment
     */
    public ServiceEndpoints getServiceEndpoints(String environment) {
        Assert.notEmpty(endpoints, "No service endpoints found");

        if (!endpoints.containsKey(environment)) {
            throw new IllegalArgumentException(environment + " is not found in the configuration," +
                    " only following environments are supported: " + endpoints.keySet());
        }

        return endpoints.get(environment);
    }
}


/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for
 * license information.
 */
package sample.cloudfoundry.servicebus;

import com.microsoft.azure.servicebus.*;
import com.microsoft.azure.servicebus.primitives.ServiceBusException;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletResponse;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;

@RestController
public class ServiceBusRestController {

    private static final Logger LOG = LoggerFactory
            .getLogger(ServiceBusRestController.class);

    private static final String CR = "</BR>";
    private static StringBuffer currentResult = null;
    @Autowired
    private QueueClient queueClientForSending;
    @Autowired
    private QueueClient queueClientForReceiving;

    @PostConstruct
    private void postConstruct() {
        LOG.debug("postConstruct start...");
        try {
            LOG.debug("registering queue handler...");
            queueClientForReceiving.registerMessageHandler(new MessageHandler(),
                    new MessageHandlerOptions());
            LOG.debug("done registering handlers...");
        } catch (InterruptedException e) {
            LOG.error("Error registering message handler", e);
        } catch (ServiceBusException e) {
            LOG.error("Error registering message handler", e);
        }
        LOG.debug("postConstruct end.");
    }

    @RequestMapping(value = "/sb", method = RequestMethod.GET)
    @ResponseBody
    @SuppressFBWarnings("ST_WRITE_TO_STATIC_FROM_INSTANCE_METHOD")
    public String processMessages(HttpServletResponse response) {
        final StringBuffer result = new StringBuffer();
        currentResult = result;
        result.append("starting..." + CR);
        try {
            result.append("sending queue message" + CR);
            sendQueueMessage();
            result.append("receiving queue message" + CR);
            Thread.sleep(2000);

            result.append("done!" + CR);

        } catch (ServiceBusException e) {
            LOG.error("Error processing messages", e);
        } catch (InterruptedException e) {
            LOG.error("Error processing messages", e);
        } finally {
            currentResult = null;
        }

        return result.toString();
    }

    // NOTE: Please be noted that below are the minimum code for demonstrating
    // the usage of autowired clients.
    // For complete documentation of Service Bus, reference
    // https://azure.microsoft.com/en-us/services/service-bus/
    private void sendQueueMessage() throws ServiceBusException,
            InterruptedException {
        final String messageBody = "queue message";
        LOG.debug("Sending message: " + messageBody);
        final Message message = new Message(
                messageBody.getBytes(StandardCharsets.UTF_8));
        queueClientForSending.send(message);
    }

    static class MessageHandler implements IMessageHandler {
        public CompletableFuture<Void> onMessageAsync(IMessage message) {
            final String messageString = new String(message.getBody(),
                    StandardCharsets.UTF_8);
            LOG.debug("Received message: " + messageString);
            if (currentResult != null) {
                currentResult.append("Received message: " + messageString + CR);
            }
            return CompletableFuture.completedFuture(null);
        }

        public void notifyException(Throwable exception, ExceptionPhase phase) {
            LOG.error(phase + " encountered exception:", exception);
        }
    }
}


/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for
 * license information.
 */
package com.microsoft.azure.spring.autoconfigure.aad;

import com.microsoft.azure.telemetry.TelemetrySender;
import com.nimbusds.jose.util.DefaultResourceRetriever;
import com.nimbusds.jose.util.ResourceRetriever;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.config.BeanDefinition;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.Scope;
import org.springframework.util.ClassUtils;

import javax.annotation.PostConstruct;
import java.util.HashMap;
import java.util.Map;

import static com.microsoft.azure.telemetry.TelemetryData.SERVICE_NAME;
import static com.microsoft.azure.telemetry.TelemetryData.getClassPackageSimpleName;

@Configuration
@ConditionalOnWebApplication
@ConditionalOnProperty(prefix = "azure.activedirectory", value = {"client-id", "client-secret"})
@EnableConfigurationProperties({AADAuthenticationProperties.class, ServiceEndpointsProperties.class})
@PropertySource(value = "classpath:serviceEndpoints.properties")
public class AADAuthenticationFilterAutoConfiguration {
    private static final Logger LOG = LoggerFactory.getLogger(AADAuthenticationProperties.class);

    private final AADAuthenticationProperties aadAuthProps;

    private final ServiceEndpointsProperties serviceEndpointsProps;

    public AADAuthenticationFilterAutoConfiguration(AADAuthenticationProperties aadAuthFilterProps,
                                                    ServiceEndpointsProperties serviceEndpointsProps) {
        this.aadAuthProps = aadAuthFilterProps;
        this.serviceEndpointsProps = serviceEndpointsProps;
    }

    /**
     * Declare AADAuthenticationFilter bean.
     *
     * @return AADAuthenticationFilter bean
     */
    @Bean
    @Scope(BeanDefinition.SCOPE_SINGLETON)
    @ConditionalOnMissingBean(AADAuthenticationFilter.class)
    public AADAuthenticationFilter azureADJwtTokenFilter() {
        LOG.info("AzureADJwtTokenFilter Constructor.");
        return new AADAuthenticationFilter(aadAuthProps, serviceEndpointsProps, getJWTResourceRetriever());
    }

    @Bean
    @Scope(BeanDefinition.SCOPE_SINGLETON)
    @ConditionalOnMissingBean(ResourceRetriever.class)
    public ResourceRetriever getJWTResourceRetriever() {
        return new DefaultResourceRetriever(aadAuthProps.getJwtConnectTimeout(), aadAuthProps.getJwtReadTimeout(),
                aadAuthProps.getJwtSizeLimit());
    }

    @PostConstruct
    private void sendTelemetry() {
        if (aadAuthProps.isAllowTelemetry()) {
            final Map<String, String> events = new HashMap<>();
            final TelemetrySender sender = new TelemetrySender();

            events.put(SERVICE_NAME, getClassPackageSimpleName(AADAuthenticationFilterAutoConfiguration.class));

            sender.send(ClassUtils.getUserClass(getClass()).getSimpleName(), events);
        }
    }
}


/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for
 * license information.
 */
package sample.aad.controller;

import com.microsoft.azure.spring.autoconfigure.aad.UserGroup;
import com.microsoft.azure.spring.autoconfigure.aad.UserPrincipal;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.web.authentication.preauth.PreAuthenticatedAuthenticationToken;
import org.springframework.web.bind.annotation.*;
import sample.aad.model.TodoItem;

import java.util.*;
import java.util.stream.Collectors;

@RestController
public class TodolistController {
    private final List<TodoItem> todoList = new ArrayList<>();

    public TodolistController() {
        todoList.add(0, new TodoItem(2398, "anything", "whoever"));
    }

    @RequestMapping("/home")
    public Map<String, Object> home() {
        final Map<String, Object> model = new HashMap<>();
        model.put("id", UUID.randomUUID().toString());
        model.put("content", "home");
        return model;
    }

    /**
     * HTTP GET
     */
    @RequestMapping(value = "/api/todolist/{index}",
            method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
    public ResponseEntity<?> getTodoItem(@PathVariable("index") int index) {
        if (index > todoList.size() - 1) {
            return new ResponseEntity<>(new TodoItem(-1, "index out of range", null), HttpStatus.NOT_FOUND);
        }
        return new ResponseEntity<>(todoList.get(index), HttpStatus.OK);
    }

    /**
     * HTTP GET ALL
     */
    @RequestMapping(value = "/api/todolist", method = RequestMethod.GET, produces = {MediaType.APPLICATION_JSON_VALUE})
    public ResponseEntity<List<TodoItem>> getAllTodoItems() {
        return new ResponseEntity<>(todoList, HttpStatus.OK);
    }

    @PreAuthorize("hasRole('ROLE_group1')")
    @RequestMapping(value = "/api/todolist", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> addNewTodoItem(@RequestBody TodoItem item) {
        item.setID(todoList.size() + 1);
        todoList.add(todoList.size(), item);
        return new ResponseEntity<>("Entity created", HttpStatus.CREATED);
    }

    /**
     * HTTP PUT
     */
    @PreAuthorize("hasRole('ROLE_group1')")
    @RequestMapping(value = "/api/todolist", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE)
    public ResponseEntity<String> updateTodoItem(@RequestBody TodoItem item) {
        final List<TodoItem> find =
                todoList.stream().filter(i -> i.getID() == item.getID()).collect(Collectors.toList());
        if (!find.isEmpty()) {
            todoList.set(todoList.indexOf(find.get(0)), item);
            return new ResponseEntity<>("Entity is updated", HttpStatus.OK);
        }
        return new ResponseEntity<>("Entity not found", HttpStatus.OK);
    }

    /**
     * HTTP DELETE
     */
    @RequestMapping(value = "/api/todolist/{id}", method = RequestMethod.DELETE)
    public ResponseEntity<String> deleteTodoItem(@PathVariable("id") int id,
                                                 PreAuthenticatedAuthenticationToken authToken) {
        final UserPrincipal current = (UserPrincipal) authToken.getPrincipal();

        if (current.isMemberOf(
                new UserGroup("xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", "group1"))) {
            final List<TodoItem> find = todoList.stream().filter(i -> i.getID() == id).collect(Collectors.toList());
            if (!find.isEmpty()) {
                todoList.remove(todoList.indexOf(find.get(0)));
                return new ResponseEntity<>("OK", HttpStatus.OK);
            }
            return new ResponseEntity<>("Entity not found", HttpStatus.OK);
        } else {
            return new ResponseEntity<>("Access is denied", HttpStatus.OK);
        }

    }
}


/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for
 * license information.
 */

package sample.mediaservices;

import com.microsoft.windowsazure.exception.ServiceException;
import com.microsoft.windowsazure.services.media.MediaContract;
import com.microsoft.windowsazure.services.media.WritableBlobContainerContract;
import com.microsoft.windowsazure.services.media.models.*;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.core.io.ClassPathResource;

import java.io.IOException;
import java.io.InputStream;
import java.util.EnumSet;

@SpringBootApplication
public class MediaServicesSampleApplication implements CommandLineRunner {

    @Autowired
    private MediaContract mediaService;

    public static void main(String[] args) {
        SpringApplication.run(MediaServicesSampleApplication.class);
    }

    // Note: Here is the minimum sample code that demonstrates how MediaContract is
    // autowired and used.
    // More information can be found at
    // https://docs.microsoft.com/en-us/azure/media-services/media-services-java-how-to-use
    @Override
    public void run(String... args) throws Exception {
        // Upload a local file to an Asset
        final AssetInfo uploadAsset = uploadFileAndCreateAsset("video.mp4");
        System.out.println("Uploaded Asset Id: " + uploadAsset.getId());
        System.out.println("Sample completed!");
    }

    private AssetInfo uploadFileAndCreateAsset(String fileName)
            throws ServiceException, IOException {
        final WritableBlobContainerContract uploader;
        final AssetInfo resultAsset;
        final AccessPolicyInfo uploadAccessPolicy;
        LocatorInfo uploadLocator = null;

        // Create an Asset
        resultAsset = mediaService
                .create(Asset.create().setName(fileName).setAlternateId("altId"));
        System.out.println("Created Asset " + fileName);

        // Create an AccessPolicy that provides Write access for 15 minutes
        uploadAccessPolicy = mediaService.create(AccessPolicy.create("uploadAccessPolicy",
                15.0, EnumSet.of(AccessPolicyPermission.WRITE)));

        // Create a Locator using the AccessPolicy and Asset
        uploadLocator = mediaService.create(Locator.create(uploadAccessPolicy.getId(),
                resultAsset.getId(), LocatorType.SAS));

        // Create the Blob Writer using the Locator
        uploader = mediaService.createBlobWriter(uploadLocator);

        // The local file that will be uploaded to your Media Services account
        try (final InputStream input = new ClassPathResource(fileName).getInputStream()) {
            System.out.println("Uploading " + fileName);

            // Upload the local file to the asset
            uploader.createBlockBlob(fileName, input);
        }
        // Inform Media Services about the uploaded files
        mediaService.action(AssetFile.createFileInfos(resultAsset.getId()));
        System.out.println("Uploaded Asset File " + fileName);

        mediaService.delete(Locator.delete(uploadLocator.getId()));
        mediaService.delete(AccessPolicy.delete(uploadAccessPolicy.getId()));

        return resultAsset;
    }

}


/**
 * Copyright (c) Microsoft Corporation. All rights reserved.
 * Licensed under the MIT License. See LICENSE in the project root for
 * license information.
 */
package sample.servicebus;

import com.microsoft.azure.servicebus.*;
import com.microsoft.azure.servicebus.primitives.ServiceBusException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import java.nio.charset.StandardCharsets;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

@SpringBootApplication
public class ServiceBusSampleApplication implements CommandLineRunner {

    @Autowired
    private QueueClient queueClient;
    @Autowired
    private TopicClient topicClient;
    @Autowired
    private SubscriptionClient subscriptionClient;

    public static void main(String[] args) {
        SpringApplication.run(ServiceBusSampleApplication.class);
    }

    public void run(String... var1) throws ServiceBusException, InterruptedException {
        sendQueueMessage();
        receiveQueueMessage();

        sendTopicMessage();
        receiveSubscriptionMessage();
    }

    // NOTE: Please be noted that below are the minimum code for demonstrating the usage of autowired clients.
    // For complete documentation of Service Bus, reference https://azure.microsoft.com/en-us/services/service-bus/
    private void sendQueueMessage() throws ServiceBusException, InterruptedException {
        final String messageBody = "queue message";
        System.out.println("Sending message: " + messageBody);
        final Message message = new Message(messageBody.getBytes(StandardCharsets.UTF_8));
        queueClient.send(message);
    }

    private void receiveQueueMessage() throws ServiceBusException, InterruptedException {
        queueClient.registerMessageHandler(new MessageHandler(), new MessageHandlerOptions());

        TimeUnit.SECONDS.sleep(5);
        queueClient.close();
    }

    private void sendTopicMessage() throws ServiceBusException, InterruptedException {
        final String messageBody = "topic message";
        System.out.println("Sending message: " + messageBody);
        final Message message = new Message(messageBody.getBytes(StandardCharsets.UTF_8));
        topicClient.send(message);
        topicClient.close();
    }

    private void receiveSubscriptionMessage() throws ServiceBusException, InterruptedException {
        subscriptionClient.registerMessageHandler(new MessageHandler(), new MessageHandlerOptions());

        TimeUnit.SECONDS.sleep(5);
        subscriptionClient.close();
    }

    static class MessageHandler implements IMessageHandler {
        public CompletableFuture<Void> onMessageAsync(IMessage message) {
            final String messageString = new String(message.getBody(), StandardCharsets.UTF_8);
            System.out.println("Received message: " + messageString);
            return CompletableFuture.completedFuture(null);
        }

        public void notifyException(Throwable exception, ExceptionPhase phase) {
            System.out.println(phase + " encountered exception:" + exception.getMessage());
        }
    }
}


/**
 *                                  Apache License
 *                            Version 2.0, January 2004
 *                         http://www.apache.org/licenses/
 *
 *    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 *
 *    1. Definitions.
 *
 *       "License" shall mean the terms and conditions for use, reproduction,
 *       and distribution as defined by Sections 1 through 9 of this document.
 *
 *       "Licensor" shall mean the copyright owner or entity authorized by
 *       the copyright owner that is granting the License.
 *
 *       "Legal Entity" shall mean the union of the acting entity and all
 *       other entities that control, are controlled by, or are under common
 *       control with that entity. For the purposes of this definition,
 *       "control" means (i) the power, direct or indirect, to cause the
 *       direction or management of such entity, whether by contract or
 *       otherwise, or (ii) ownership of fifty percent (50%) or more of the
 *       outstanding shares, or (iii) beneficial ownership of such entity.
 *
 *       "You" (or "Your") shall mean an individual or Legal Entity
 *       exercising permissions granted by this License.
 *
 *       "Source" form shall mean the preferred form for making modifications,
 *       including but not limited to software source code, documentation
 *       source, and configuration files.
 *
 *       "Object" form shall mean any form resulting from mechanical
 *       transformation or translation of a Source form, including but
 *       not limited to compiled object code, generated documentation,
 *       and conversions to other media types.
 *
 *       "Work" shall mean the work of authorship, whether in Source or
 *       Object form, made available under the License, as indicated by a
 *       copyright notice that is included in or attached to the work
 *       (an example is provided in the Appendix below).
 *
 *       "Derivative Works" shall mean any work, whether in Source or Object
 *       form, that is based on (or derived from) the Work and for which the
 *       editorial revisions, annotations, elaborations, or other modifications
 *       represent, as a whole, an original work of authorship. For the purposes
 *       of this License, Derivative Works shall not include works that remain
 *       separable from, or merely link (or bind by name) to the interfaces of,
 *       the Work and Derivative Works thereof.
 *
 *       "Contribution" shall mean any work of authorship, including
 *       the original version of the Work and any modifications or additions
 *       to that Work or Derivative Works thereof, that is intentionally
 *       submitted to Licensor for inclusion in the Work by the copyright owner
 *       or by an individual or Legal Entity authorized to submit on behalf of
 *       the copyright owner. For the purposes of this definition, "submitted"
 *       means any form of electronic, verbal, or written communication sent
 *       to the Licensor or its representatives, including but not limited to
 *       communication on electronic mailing lists, source code control systems,
 *       and issue tracking systems that are managed by, or on behalf of, the
 *       Licensor for the purpose of discussing and improving the Work, but
 *       excluding communication that is conspicuously marked or otherwise
 *       designated in writing by the copyright owner as "Not a Contribution."
 *
 *       "Contributor" shall mean Licensor and any individual or Legal Entity
 *       on behalf of whom a Contribution has been received by Licensor and
 *       subsequently incorporated within the Work.
 *
 *    2. Grant of Copyright License. Subject to the terms and conditions of
 *       this License, each Contributor hereby grants to You a perpetual,
 *       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 *       copyright license to reproduce, prepare Derivative Works of,
 *       publicly display, publicly perform, sublicense, and distribute the
 *       Work and such Derivative Works in Source or Object form.
 *
 *    3. Grant of Patent License. Subject to the terms and conditions of
 *       this License, each Contributor hereby grants to You a perpetual,
 *       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 *       (except as stated in this section) patent license to make, have made,
 *       use, offer to sell, sell, import, and otherwise transfer the Work,
 *       where such license applies only to those patent claims licensable
 *       by such Contributor that are necessarily infringed by their
 *       Contribution(s) alone or by combination of their Contribution(s)
 *       with the Work to which such Contribution(s) was submitted. If You
 *       institute patent litigation against any entity (including a
 *       cross-claim or counterclaim in a lawsuit) alleging that the Work
 *       or a Contribution incorporated within the Work constitutes direct
 *       or contributory patent infringement, then any patent licenses
 *       granted to You under this License for that Work shall terminate
 *       as of the date such litigation is filed.
 *
 *    4. Redistribution. You may reproduce and distribute copies of the
 *       Work or Derivative Works thereof in any medium, with or without
 *       modifications, and in Source or Object form, provided that You
 *       meet the following conditions:
 *
 *       (a) You must give any other recipients of the Work or
 *           Derivative Works a copy of this License; and
 *
 *       (b) You must cause any modified files to carry prominent notices
 *           stating that You changed the files; and
 *
 *       (c) You must retain, in the Source form of any Derivative Works
 *           that You distribute, all copyright, patent, trademark, and
 *           attribution notices from the Source form of the Work,
 *           excluding those notices that do not pertain to any part of
 *           the Derivative Works; and
 *
 *       (d) If the Work includes a "NOTICE" text file as part of its
 *           distribution, then any Derivative Works that You distribute must
 *           include a readable copy of the attribution notices contained
 *           within such NOTICE file, excluding those notices that do not
 *           pertain to any part of the Derivative Works, in at least one
 *           of the following places: within a NOTICE text file distributed
 *           as part of the Derivative Works; within the Source form or
 *           documentation, if provided along with the Derivative Works; or,
 *           within a display generated by the Derivative Works, if and
 *           wherever such third-party notices normally appear. The contents
 *           of the NOTICE file are for informational purposes only and
 *           do not modify the License. You may add Your own attribution
 *           notices within Derivative Works that You distribute, alongside
 *           or as an addendum to the NOTICE text from the Work, provided
 *           that such additional attribution notices cannot be construed
 *           as modifying the License.
 *
 *       You may add Your own copyright statement to Your modifications and
 *       may provide additional or different license terms and conditions
 *       for use, reproduction, or distribution of Your modifications, or
 *       for any such Derivative Works as a whole, provided Your use,
 *       reproduction, and distribution of the Work otherwise complies with
 *       the conditions stated in this License.
 *
 *    5. Submission of Contributions. Unless You explicitly state otherwise,
 *       any Contribution intentionally submitted for inclusion in the Work
 *       by You to the Licensor shall be under the terms and conditions of
 *       this License, without any additional terms or conditions.
 *       Notwithstanding the above, nothing herein shall supersede or modify
 *       the terms of any separate license agreement you may have executed
 *       with Licensor regarding such Contributions.
 *
 *    6. Trademarks. This License does not grant permission to use the trade
 *       names, trademarks, service marks, or product names of the Licensor,
 *       except as required for reasonable and customary use in describing the
 *       origin of the Work and reproducing the content of the NOTICE file.
 *
 *    7. Disclaimer of Warranty. Unless required by applicable law or
 *       agreed to in writing, Licensor provides the Work (and each
 *       Contributor provides its Contributions) on an "AS IS" BASIS,
 *       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 *       implied, including, without limitation, any warranties or conditions
 *       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
 *       PARTICULAR PURPOSE. You are solely responsible for determining the
 *       appropriateness of using or redistributing the Work and assume any
 *       risks associated with Your exercise of permissions under this License.
 *
 *    8. Limitation of Liability. In no event and under no legal theory,
 *       whether in tort (including negligence), contract, or otherwise,
 *       unless required by applicable law (such as deliberate and grossly
 *       negligent acts) or agreed to in writing, shall any Contributor be
 *       liable to You for damages, including any direct, indirect, special,
 *       incidental, or consequential damages of any character arising as a
 *       result of this License or out of the use or inability to use the
 *       Work (including but not limited to damages for loss of goodwill,
 *       work stoppage, computer failure or malfunction, or any and all
 *       other commercial damages or losses), even if such Contributor
 *       has been advised of the possibility of such damages.
 *
 *    9. Accepting Warranty or Additional Liability. While redistributing
 *       the Work or Derivative Works thereof, You may choose to offer,
 *       and charge a fee for, acceptance of support, warranty, indemnity,
 *       or other liability obligations and/or rights consistent with this
 *       License. However, in accepting such obligations, You may act only
 *       on Your own behalf and on Your sole responsibility, not on behalf
 *       of any other Contributor, and only if You agree to indemnify,
 *       defend, and hold each Contributor harmless for any liability
 *       incurred by, or claims asserted against, such Contributor by reason
 *       of your accepting any such warranty or additional liability.
 *
 *    END OF TERMS AND CONDITIONS
 *
 *    APPENDIX: How to apply the Apache License to your work.
 *
 *       To apply the Apache License to your work, attach the following
 *       boilerplate notice, with the fields enclosed by brackets "{}"
 *       replaced with your own identifying information. (Don't include
 *       the brackets!)  The text should be enclosed in the appropriate
 *       comment syntax for the file format. We also recommend that a
 *       file or class name and description of purpose be included on the
 *       same "printed page" as the copyright notice for easier
 *       identification within third-party archives.
 *
 *    Copyright 2014 Edgar Espina
 *
 *    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.jooby;

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.Optional;

import com.typesafe.config.Config;
import org.jooby.funzy.Try;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.google.common.base.Throwables;

import javax.annotation.Nullable;

/**
 * An exception that carry a {@link Status}. The status field will be set in the HTTP
 * response.
 *
 * See {@link Err.Handler} for more details on how to deal with exceptions.
 *
 * @author edgar
 * @since 0.1.0
 */
@SuppressWarnings("serial")
public class Err extends RuntimeException {

  /**
   * Exception thrown from {@link MediaType#parse(String)} in case of encountering an invalid media
   * type specification String.
   *
   * @author edgar
   */
  public static class BadMediaType extends Err {

    /**
     * Creates a new {@link BadMediaType}.
     *
     * @param message Error message.
     */
    public BadMediaType(final String message) {
      super(Status.BAD_REQUEST, message);
    }

  }

  /**
   * Missing parameter/header or request attribute.
   *
   * @author edgar
   */
  public static class Missing extends Err {

    /**
     * Creates a new {@link Missing} error.
     *
     * @param name Name of the missing parameter/header or request attribute.
     */
    public Missing(final String name) {
      super(Status.BAD_REQUEST, name);
    }

  }

  /**
   * Default err handler it does content negotation.
   *
   * On text/html requests the err handler creates an err view and use the
   * {@link Err#toMap()} result as model.
   *
   * @author edgar
   * @since 0.1.0
   */
  public static class DefHandler implements Err.Handler {

    /** Default err view. */
    public static final String VIEW = "err";

    /** logger, logs!. */
    private final Logger log = LoggerFactory.getLogger(Err.class);

    @Override
    public void handle(final Request req, final Response rsp, final Err ex) throws Throwable {
      log.error("execution of: {}{} resulted in exception\nRoute:\n{}\n\nStacktrace:",
          req.method(), req.path(), req.route().print(6), ex);
      Config conf = req.require(Config.class);
      boolean stackstrace = Try.apply(() -> conf.getBoolean("err.stacktrace"))
          .orElse(req.require(Env.class).name().equals("dev"));
      rsp.send(
          Results
              .when(MediaType.html, () -> Results.html(VIEW).put("err", ex.toMap(stackstrace)))
              .when(MediaType.all, () -> ex.toMap(stackstrace)));
    }

  }

  /**
   * Handle and render exceptions. Error handlers are executed in the order they were provided, the
   * first err handler that send an output wins!
   *
   * The default err handler does content negotation on error, see {@link DefHandler}.
   *
   * @author edgar
   * @since 0.1.0
   */
  public interface Handler {

    /**
     * Handle a route exception by properly logging the error and sending a err response to the
     * client.
     *
     * Please note you always get an {@link Err} whenever you throw it or not. For example if your
     * application throws an {@link IllegalArgumentException} exception you will get an {@link Err}
     * and you can retrieve the original exception by calling {@link Err#getCause()}.
     *
     * Jooby always give you an {@link Err} with an optional root cause and an associated status
     * code.
     *
     * @param req HTTP request.
     * @param rsp HTTP response.
     * @param ex Error found and status code.
     * @throws Throwable If something goes wrong.
     */
    void handle(Request req, Response rsp, Err ex) throws Throwable;
  }

  /**
   * The status code. Required.
   */
  private int status;

  /**
   * Creates a new {@link Err}.
   *
   * @param status A HTTP status. Required.
   * @param message A error message. Required.
   * @param cause The cause of the problem.
   */
  public Err(final Status status, final String message, final Throwable cause) {
    super(message(status, message), cause);
    this.status = status.value();
  }

  /**
   * Creates a new {@link Err}.
   *
   * @param status A HTTP status. Required.
   * @param message A error message. Required.
   * @param cause The cause of the problem.
   */
  public Err(final int status, final String message, final Throwable cause) {
    super(message("", status, message), cause);
    this.status = status;
  }

  /**
   * Creates a new {@link Err}.
   *
   * @param status A web socket close status. Required.
   * @param message Close message.
   */
  public Err(final WebSocket.CloseStatus status, final String message) {
    super(message(status.reason(), status.code(), message));
    this.status = status.code();
  }

  /**
   * Creates a new {@link Err}.
   *
   * @param status A HTTP status. Required.
   * @param message A error message. Required.
   */
  public Err(final Status status, final String message) {
    super(message(status, message));
    this.status = status.value();
  }

  /**
   * Creates a new {@link Err}.
   *
   * @param status A HTTP status. Required.
   * @param message A error message. Required.
   */
  public Err(final int status, final String message) {
    this(Status.valueOf(status), message);
  }

  /**
   * Creates a new {@link Err}.
   *
   * @param status A HTTP status. Required.
   * @param cause The cause of the problem.
   */
  public Err(final Status status, final Throwable cause) {
    super(message(status, null), cause);
    this.status = status.value();
  }

  /**
   * Creates a new {@link Err}.
   *
   * @param status A HTTP status. Required.
   * @param cause The cause of the problem.
   */
  public Err(final int status, final Throwable cause) {
    this(Status.valueOf(status), cause);
  }

  /**
   * Creates a new {@link Err}.
   *
   * @param status A HTTP status. Required.
   */
  public Err(final Status status) {
    super(message(status, null));
    this.status = status.value();
  }

  /**
   * Creates a new {@link Err}.
   *
   * @param status A HTTP status. Required.
   */
  public Err(final int status) {
    this(Status.valueOf(status));
  }

  /**
   * @return The status code to send as response.
   */
  public int statusCode() {
    return status;
  }

  /**
   * Produces a friendly view of the err, resulting map has these attributes:
   *
   *

   *  message: exception message (if present)
   *  status: status code
   *  reason: a status code reason
   *

   *
   * @return A lightweight view of the err.
   */
  public Map<String, Object> toMap() {
    return toMap(false);
  }

  /**
   * Produces a friendly view of the err, resulting map has these attributes:
   *
   *

   *  message: exception message (if present)
   *  stacktrace: array with the stacktrace
   *  status: status code
   *  reason: a status code reason
   *

   *
   * @param stacktrace True for adding stacktrace.
   * @return A lightweight view of the err.
   */
  public Map<String, Object> toMap(boolean stacktrace) {
    Status status = Status.valueOf(this.status);
    Throwable cause = Optional.ofNullable(getCause()).orElse(this);
    String message = Optional.ofNullable(cause.getMessage()).orElse(status.reason());

    Map<String, Object> err = new LinkedHashMap<>();
    err.put("message", message);
    if (stacktrace) {
      err.put("stacktrace", Throwables.getStackTraceAsString(cause).replace("\r", "").split("\\n"));
    }
    err.put("status", status.value());
    err.put("reason", status.reason());

    return err;
  }

  /**
   * Build an error message using the HTTP status.
   *
   * @param status The HTTP Status.
   * @param tail A message to append.
   * @return An error message.
   */
  private static String message(final Status status, @Nullable final String tail) {
    return message(status.reason(), status.value(), tail);
  }

  /**
   * Build an error message using the HTTP status.
   *
   * @param reason Reason.
   * @param status The Status.
   * @param tail A message to append.
   * @return An error message.
   */
  private static String message(final String reason, final int status, @Nullable  final String tail) {
    return reason + "(" + status + ")" + (tail == null ? "" : ": " + tail);
  }

}


/**
 *                                  Apache License
 *                            Version 2.0, January 2004
 *                         http://www.apache.org/licenses/
 *
 *    TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
 *
 *    1. Definitions.
 *
 *       "License" shall mean the terms and conditions for use, reproduction,
 *       and distribution as defined by Sections 1 through 9 of this document.
 *
 *       "Licensor" shall mean the copyright owner or entity authorized by
 *       the copyright owner that is granting the License.
 *
 *       "Legal Entity" shall mean the union of the acting entity and all
 *       other entities that control, are controlled by, or are under common
 *       control with that entity. For the purposes of this definition,
 *       "control" means (i) the power, direct or indirect, to cause the
 *       direction or management of such entity, whether by contract or
 *       otherwise, or (ii) ownership of fifty percent (50%) or more of the
 *       outstanding shares, or (iii) beneficial ownership of such entity.
 *
 *       "You" (or "Your") shall mean an individual or Legal Entity
 *       exercising permissions granted by this License.
 *
 *       "Source" form shall mean the preferred form for making modifications,
 *       including but not limited to software source code, documentation
 *       source, and configuration files.
 *
 *       "Object" form shall mean any form resulting from mechanical
 *       transformation or translation of a Source form, including but
 *       not limited to compiled object code, generated documentation,
 *       and conversions to other media types.
 *
 *       "Work" shall mean the work of authorship, whether in Source or
 *       Object form, made available under the License, as indicated by a
 *       copyright notice that is included in or attached to the work
 *       (an example is provided in the Appendix below).
 *
 *       "Derivative Works" shall mean any work, whether in Source or Object
 *       form, that is based on (or derived from) the Work and for which the
 *       editorial revisions, annotations, elaborations, or other modifications
 *       represent, as a whole, an original work of authorship. For the purposes
 *       of this License, Derivative Works shall not include works that remain
 *       separable from, or merely link (or bind by name) to the interfaces of,
 *       the Work and Derivative Works thereof.
 *
 *       "Contribution" shall mean any work of authorship, including
 *       the original version of the Work and any modifications or additions
 *       to that Work or Derivative Works thereof, that is intentionally
 *       submitted to Licensor for inclusion in the Work by the copyright owner
 *       or by an individual or Legal Entity authorized to submit on behalf of
 *       the copyright owner. For the purposes of this definition, "submitted"
 *       means any form of electronic, verbal, or written communication sent
 *       to the Licensor or its representatives, including but not limited to
 *       communication on electronic mailing lists, source code control systems,
 *       and issue tracking systems that are managed by, or on behalf of, the
 *       Licensor for the purpose of discussing and improving the Work, but
 *       excluding communication that is conspicuously marked or otherwise
 *       designated in writing by the copyright owner as "Not a Contribution."
 *
 *       "Contributor" shall mean Licensor and any individual or Legal Entity
 *       on behalf of whom a Contribution has been received by Licensor and
 *       subsequently incorporated within the Work.
 *
 *    2. Grant of Copyright License. Subject to the terms and conditions of
 *       this License, each Contributor hereby grants to You a perpetual,
 *       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 *       copyright license to reproduce, prepare Derivative Works of,
 *       publicly display, publicly perform, sublicense, and distribute the
 *       Work and such Derivative Works in Source or Object form.
 *
 *    3. Grant of Patent License. Subject to the terms and conditions of
 *       this License, each Contributor hereby grants to You a perpetual,
 *       worldwide, non-exclusive, no-charge, royalty-free, irrevocable
 *       (except as stated in this section) patent license to make, have made,
 *       use, offer to sell, sell, import, and otherwise transfer the Work,
 *       where such license applies only to those patent claims licensable
 *       by such Contributor that are necessarily infringed by their
 *       Contribution(s) alone or by combination of their Contribution(s)
 *       with the Work to which such Contribution(s) was submitted. If You
 *       institute patent litigation against any entity (including a
 *       cross-claim or counterclaim in a lawsuit) alleging that the Work
 *       or a Contribution incorporated within the Work constitutes direct
 *       or contributory patent infringement, then any patent licenses
 *       granted to You under this License for that Work shall terminate
 *       as of the date such litigation is filed.
 *
 *    4. Redistribution. You may reproduce and distribute copies of the
 *       Work or Derivative Works thereof in any medium, with or without
 *       modifications, and in Source or Object form, provided that You
 *       meet the following conditions:
 *
 *       (a) You must give any other recipients of the Work or
 *           Derivative Works a copy of this License; and
 *
 *       (b) You must cause any modified files to carry prominent notices
 *           stating that You changed the files; and
 *
 *       (c) You must retain, in the Source form of any Derivative Works
 *           that You distribute, all copyright, patent, trademark, and
 *           attribution notices from the Source form of the Work,
 *           excluding those notices that do not pertain to any part of
 *           the Derivative Works; and
 *
 *       (d) If the Work includes a "NOTICE" text file as part of its
 *           distribution, then any Derivative Works that You distribute must
 *           include a readable copy of the attribution notices contained
 *           within such NOTICE file, excluding those notices that do not
 *           pertain to any part of the Derivative Works, in at least one
 *           of the following places: within a NOTICE text file distributed
 *           as part of the Derivative Works; within the Source form or
 *           documentation, if provided along with the Derivative Works; or,
 *           within a display generated by the Derivative Works, if and
 *           wherever such third-party notices normally appear. The contents
 *           of the NOTICE file are for informational purposes only and
 *           do not modify the License. You may add Your own attribution
 *           notices within Derivative Works that You distribute, alongside
 *           or as an addendum to the NOTICE text from the Work, provided
 *           that such additional attribution notices cannot be construed
 *           as modifying the License.
 *
 *       You may add Your own copyright statement to Your modifications and
 *       may provide additional or different license terms and conditions
 *       for use, reproduction, or distribution of Your modifications, or
 *       for any such Derivative Works as a whole, provided Your use,
 *       reproduction, and distribution of the Work otherwise complies with
 *       the conditions stated in this License.
 *
 *    5. Submission of Contributions. Unless You explicitly state otherwise,
 *       any Contribution intentionally submitted for inclusion in the Work
 *       by You to the Licensor shall be under the terms and conditions of
 *       this License, without any additional terms or conditions.
 *       Notwithstanding the above, nothing herein shall supersede or modify
 *       the terms of any separate license agreement you may have executed
 *       with Licensor regarding such Contributions.
 *
 *    6. Trademarks. This License does not grant permission to use the trade
 *       names, trademarks, service marks, or product names of the Licensor,
 *       except as required for reasonable and customary use in describing the
 *       origin of the Work and reproducing the content of the NOTICE file.
 *
 *    7. Disclaimer of Warranty. Unless required by applicable law or
 *       agreed to in writing, Licensor provides the Work (and each
 *       Contributor provides its Contributions) on an "AS IS" BASIS,
 *       WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
 *       implied, including, without limitation, any warranties or conditions
 *       of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
 *       PARTICULAR PURPOSE. You are solely responsible for determining the
 *       appropriateness of using or redistributing the Work and assume any
 *       risks associated with Your exercise of permissions under this License.
 *
 *    8. Limitation of Liability. In no event and under no legal theory,
 *       whether in tort (including negligence), contract, or otherwise,
 *       unless required by applicable law (such as deliberate and grossly
 *       negligent acts) or agreed to in writing, shall any Contributor be
 *       liable to You for damages, including any direct, indirect, special,
 *       incidental, or consequential damages of any character arising as a
 *       result of this License or out of the use or inability to use the
 *       Work (including but not limited to damages for loss of goodwill,
 *       work stoppage, computer failure or malfunction, or any and all
 *       other commercial damages or losses), even if such Contributor
 *       has been advised of the possibility of such damages.
 *
 *    9. Accepting Warranty or Additional Liability. While redistributing
 *       the Work or Derivative Works thereof, You may choose to offer,
 *       and charge a fee for, acceptance of support, warranty, indemnity,
 *       or other liability obligations and/or rights consistent with this
 *       License. However, in accepting such obligations, You may act only
 *       on Your own behalf and on Your sole responsibility, not on behalf
 *       of any other Contributor, and only if You agree to indemnify,
 *       defend, and hold each Contributor harmless for any liability
 *       incurred by, or claims asserted against, such Contributor by reason
 *       of your accepting any such warranty or additional liability.
 *
 *    END OF TERMS AND CONDITIONS
 *
 *    APPENDIX: How to apply the Apache License to your work.
 *
 *       To apply the Apache License to your work, attach the following
 *       boilerplate notice, with the fields enclosed by brackets "{}"
 *       replaced with your own identifying information. (Don't include
 *       the brackets!)  The text should be enclosed in the appropriate
 *       comment syntax for the file format. We also recommend that a
 *       file or class name and description of purpose be included on the
 *       same "printed page" as the copyright notice for easier
 *       identification within third-party archives.
 *
 *    Copyright 2014 Edgar Espina
 *
 *    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.jooby.livereload;

import com.google.common.base.CaseFormat;
import com.google.common.collect.ImmutableSet;
import com.google.inject.Binder;
import com.typesafe.config.Config;
import org.jooby.Env;
import org.jooby.Jooby.Module;
import org.jooby.MediaType;
import org.jooby.Request;
import org.jooby.Route;
import org.jooby.Router;
import org.jooby.WebSocket;
import org.jooby.filewatcher.FileWatcher;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Predicate;

/**
 *
liveReload

 *

 * LiveReload monitors changes in the file system. As soon as
 * you save a file, it is preprocessed as
 * needed, and the browser is refreshed.
 *

 *

 * Even cooler, when you change a CSS file or an image, the browser is updated instantly without
 * reloading the page.
 *

 *
 *
usage

 *
{@code
 * {
 *   use(new Jackson());
 *
 *   use(new LiveReload());
 * }
 * }

 *
 *

 * This module is available as long you run in development: application.env=dev.
 *

 *
 *
configuration

 *
 *
browser extension

 *

 * Install the LiveReload browser extension.
 *

 *
 *
livereload.js

 *

 * Add the livereload.js to your web page.
 *

 *
 *

 * You can do this manually:
 *

 *
 *
{@code
 *
 * }

 *
 *

 * Or use the liveReload local variable with your preferred template engine. Here is an
 * example using Handlebars:
 *

 *
 *

 * {{liveReload}}
 *

 *
 *
json module

 *

 * The LiveReload
 * Protocol run on top of a WebSocket and uses JSON as protocol
 * format. That's is why we also need to install a JSON module, like Jackson.
 *

 *
 *
watcher

 *

 * It automatically reload static resources from public, target (Maven
 * projects) or build folders (Gradle projects).
 *

 *
 *

 * Every time a change is detected the websocket send a reload command.
 *

 *
 *

 * That's all folks!!
 *

 *
 * @author edgar
 * @since 1.1.0
 */
public class LiveReload implements Module {

  public static final String V7 = "http://livereload.com/protocols/official-7";

  /** The logging system. */
  private final Logger log = LoggerFactory.getLogger(getClass());

  private static final Set<String> CSS = ImmutableSet.of(".css", ".scss", ".sass", ".less");

  private Predicate<Path> css = path -> CSS.stream()
      .filter(ext -> path.toString().endsWith(ext))
      .findFirst()
      .isPresent();

  private List<Object[]> paths = new ArrayList<>();

  /**
   * Add the given path to the watcher.
   *
   * @param path Path to watch.
   * @param includes Glob pattern of matching files.
   * @return This module.
   */
  public LiveReload register(final Path path, final String... includes) {
    if (Files.exists(path)) {
      paths.add(new Object[]{path, Arrays.asList(includes)});
    }
    return this;
  }

  /**
   * Test if a path is a css like file (less, sass, etc.).
   *
   * @param predicate CSS predicate.
   * @return This module.
   */
  public LiveReload liveCss(final Predicate<Path> predicate) {
    this.css = predicate;
    return this;
  }

  @SuppressWarnings("unchecked")
  @Override
  public void configure(final Env env, final Config conf, final Binder binder) throws Throwable {
    boolean enabled = conf.hasPath("livereload.enabled")
        ? conf.getBoolean("livereload.enabled")
        : "dev".equals(env.name());
    if (enabled) {
      Router router = env.router();
      /**
       * Livereload client:
       */
      String livereloadjs = "/" + LiveReload.class.getPackage().getName().replace(".", "/")
          + "/livereload.js";
      router.assets("/livereload.js", livereloadjs);
      /** {{liveReload}} local variable */
      router.use("*", (req, rsp) -> req.set("liveReload", template(req)))
          .name("livereload");

      String serverName = CaseFormat.LOWER_CAMEL
          .to(CaseFormat.UPPER_CAMEL, conf.getString("application.name"));

      Queue<WebSocket> broadcast = new ConcurrentLinkedQueue<>();
      AtomicBoolean first = new AtomicBoolean(true);
      /**
       * Websocket:
       */
      router.ws("/livereload", ws -> {
        // add to broadcast
        broadcast.add(ws);

        ws.onMessage(msg -> {
          Map<String, Object> cmd = msg.to(Map.class);
          log.debug("command: {}", cmd);

          Map<String, Object> rsp = handshake(cmd, serverName, V7);
          if (rsp != null) {
            log.debug("sending: {}", rsp);
            ws.send(rsp);
          } else {
            log.trace("ignoring command: {}", cmd);
            // resync app state after a jooby:run restart
            if (first.compareAndSet(true, false)) {
              int counter = Integer.parseInt(System.getProperty("joobyRun.counter", "0"));
              if (counter > 0) {
                ws.send(reload("/", true));
              }
            }
          }
        });

        // remove from broadcast
        ws.onClose(reason -> broadcast.remove(ws));
      }).consumes(MediaType.json).produces(MediaType.json);

      if (paths.isEmpty()) {
        Path basedir = Paths.get(System.getProperty("user.dir"));
        Path assetsDir;
        if (conf.hasPath("assets.outputDir")) {
          assetsDir = Paths.get(conf.getString("assets.outputDir"));
        } else {
          assetsDir = basedir.resolve("public");
        }
        register(basedir.resolve("public"),
            "**/*.html",
            "**/*.ftl",
            "**/*.hbs",
            "**/*.jade"
            );
        register(assetsDir,
            "**/*.css",
            "**/*.scss",
            "**/*.sass",
            "**/*.less",
            "**/*.js",
            "**/*.coffee",
            "**/*.ts");
        register(basedir.resolve("target"),
            "**/*.class",
            "**/*.conf",
            "**/*.properties");
        register(basedir.resolve("build"),
            "**/*.class",
            "**/*.conf",
            "**/*.properties");
      }

      if (paths.size() > 0) {
        FileWatcher watcher = new FileWatcher();
        paths.forEach(it -> watcher.register((Path) it[0], (kind, path) -> {
          Path relative = relative(paths, path.toAbsolutePath());
          log.debug("file changed {}: {}", relative, File.separator);
          Map<String, Object> reload = reload(
              Route.normalize("/" + relative.toString().replace(File.separator, "/")),
              css.test(relative));
          for (WebSocket ws : broadcast) {
            try {
              log.debug("sending: {}", reload);
              ws.send(reload);
            } catch (Exception x) {
              log.debug("execution of {} resulted in exception", reload, x);
            }
          }
        }, options -> ((List<String>) it[1]).forEach(options::includes)));
        watcher.configure(env, conf, binder);
      } else {
        log.warn("File watcher is off");
      }
    }
  }

  private String template(Request req) {
    String contextPath = req.contextPath();
    return "\n"
        + "";
  }

  private boolean isHello(final Map<String, Object> message) {
    return "hello".equals(message.get("command"));
  }

  @SuppressWarnings("unchecked")
  private Map<String, Object> handshake(final Map<String, Object> client,
      final String serverName, final String version) {
    if (isHello(client)) {
      List<String> protocols = (List<String>) client.get("protocols");
      return protocols.stream()
          .filter(protocol -> version.equalsIgnoreCase(protocol))
          .map(protocol -> {
            Map<String, Object> server = new LinkedHashMap<>();
            server.put("command", "hello");
            server.put("protocols", new String[]{protocol});
            server.put("serverName", serverName);
            return server;
          })
          .findFirst()
          .orElse(null);
    }
    return null;
  }

  private Path relative(final List<Object[]> paths, final Path path) {
    for (Object[] meta : paths) {
      Path root = ((Path) meta[0]).toAbsolutePath();
      Path relative = root.relativize(path);
      if (Files.exists(root.resolve(relative))) {
        return relative;
      }
    }
    return path;
  }

  private Map<String, Object> reload(final String path, final boolean liveCss) {
    Map<String, Object> cmd = new LinkedHashMap<>();
    cmd.put("command", "reload");
    cmd.put("path", path);
    cmd.put("liveCSS", liveCss);
    return cmd;
  }

}