All Downloads are FREE. Search and download functionalities are using the official Maven repository.

eu.binjr.sources.jrds.adapters.JrdsDataAdapter Maven / Gradle / Ivy

There is a newer version: 3.20.1
Show newest version
/*
 *    Copyright 2016-2020 Frederic Thevenet
 *
 *    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 eu.binjr.sources.jrds.adapters;

import com.google.gson.Gson;
import com.google.gson.JsonParseException;
import eu.binjr.common.logging.Logger;
import eu.binjr.common.xml.XmlUtils;
import eu.binjr.core.data.adapters.HttpDataAdapter;
import eu.binjr.core.data.adapters.SerializedDataAdapter;
import eu.binjr.core.data.adapters.SourceBinding;
import eu.binjr.core.data.codec.csv.CsvDecoder;
import eu.binjr.core.data.exceptions.DataAdapterException;
import eu.binjr.core.data.exceptions.FetchingDataFromAdapterException;
import eu.binjr.core.data.exceptions.InvalidAdapterParameterException;
import eu.binjr.core.data.exceptions.SourceCommunicationException;
import eu.binjr.core.data.timeseries.DoubleTimeSeriesProcessor;
import eu.binjr.core.dialogs.Dialogs;
import eu.binjr.sources.jrds.adapters.json.JsonJrdsItem;
import eu.binjr.sources.jrds.adapters.json.JsonJrdsTree;
import jakarta.xml.bind.JAXB;
import jakarta.xml.bind.annotation.XmlAccessType;
import jakarta.xml.bind.annotation.XmlAccessorType;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import org.apache.http.HttpEntity;
import org.apache.http.NameValuePair;
import org.apache.http.StatusLine;
import org.apache.http.client.HttpResponseException;
import org.apache.http.entity.ContentType;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;
import org.eclipse.fx.ui.controls.tree.FilterableTreeItem;

import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.time.Instant;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.stream.Collectors;

/**
 * This class provides an implementation of {@link SerializedDataAdapter} for JRDS.
 *
 * @author Frederic Thevenet
 */
@XmlAccessorType(XmlAccessType.FIELD)
public class JrdsDataAdapter extends HttpDataAdapter {
    private static final String JRDS_FILTER = "filter";
    private static final String JRDS_TREE = "tree";
    private static final String ENCODING_PARAM_NAME = "encoding";
    private static final String ZONE_ID_PARAM_NAME = "zoneId";
    private static final String TREE_VIEW_TAB_PARAM_NAME = "treeViewTab";
    private static final Logger logger = Logger.create(JrdsDataAdapter.class);
    private static final char DELIMITER = ',';
    private final Gson gson;
    private CsvDecoder decoder;
    private String filter;
    private ZoneId zoneId;
    private String encoding;
    private JrdsTreeViewTab treeViewTab;

    /**
     * Initialises a new instance of the {@link JrdsDataAdapter} class.
     *
     * @throws DataAdapterException if an error occurs while initializing the adapter.
     */
    public JrdsDataAdapter() throws DataAdapterException {
        this(null, ZoneId.systemDefault(), "utf-8", JrdsTreeViewTab.HOSTS_TAB, "");
    }

    /**
     * Initializes a new instance of the {@link JrdsDataAdapter} class.
     *
     * @param baseURL     the URL to the JRDS webapp.
     * @param zoneId      the id of the time zone used to record dates.
     * @param encoding    the encoding used by the download servlet.
     * @param treeViewTab the tab to apply.
     * @param filter      the filter to apply to the tree view.
     * @throws DataAdapterException if an error occurs while initializing the adapter.
     */
    public JrdsDataAdapter(URL baseURL, ZoneId zoneId, String encoding, JrdsTreeViewTab treeViewTab, String filter) throws DataAdapterException {
        super(baseURL);
        this.zoneId = zoneId;
        this.encoding = encoding;
        this.treeViewTab = treeViewTab;
        this.filter = filter;
        this.decoder = decoderFactory(zoneId);
        gson = new Gson();
    }

    /**
     * Builds a new instance of the {@link JrdsDataAdapter} class from the provided parameters.
     *
     * @param address     the URL to the JRDS webapp.
     * @param zoneId      the id of the time zone used to record dates.
     * @param treeViewTab the tab to apply.
     * @param filter      the filter to apply to the tree view.
     * @return a new instance of the {@link JrdsDataAdapter} class.
     * @throws DataAdapterException if an error occurs while initializing the adapter.
     */
    public static JrdsDataAdapter fromUrl(String address, ZoneId zoneId, JrdsTreeViewTab treeViewTab, String filter) throws DataAdapterException {
        return new JrdsDataAdapter(urlFromString(address), zoneId, "utf-8", treeViewTab, filter);
    }

    //region [DataAdapter Members]

    @Override
    public FilterableTreeItem getBindingTree() throws DataAdapterException {
        try {
            JsonJrdsTree t = gson.fromJson(getJsonTree(treeViewTab.getCommand(), treeViewTab.getArgument(), filter), JsonJrdsTree.class);
            Map m = Arrays.stream(t.items).collect(Collectors.toMap(o -> o.id, (o -> o)));
            FilterableTreeItem tree = new FilterableTreeItem<>(
                    new JrdsBindingBuilder()
                            .withLabel(getSourceName())
                            .withPath("/")
                            .withAdapter(this)
                            .build());
            for (JsonJrdsItem branch : Arrays.stream(t.items)
                    .filter(jsonJrdsItem -> JRDS_TREE.equals(jsonJrdsItem.type) || JRDS_FILTER.equals(jsonJrdsItem.type))
                    .collect(Collectors.toList())) {
                attachNode(tree, branch.id, m);
            }
            return tree;
        } catch (JsonParseException e) {
            throw new DataAdapterException("An error occurred while parsing the json response to getBindingTree request", e);
        } catch (URISyntaxException e) {
            throw new SourceCommunicationException("Error building URI for request", e);
        }
    }

    @Override
    protected URI craftFetchUri(String path, Instant begin, Instant end) throws DataAdapterException {
        return craftRequestUri("download",
                new BasicNameValuePair("id", path),
                new BasicNameValuePair("begin", Long.toString(begin.toEpochMilli())),
                new BasicNameValuePair("end", Long.toString(end.toEpochMilli()))
        );
    }

    @Override
    public String getSourceName() {
        return new StringBuilder("[JRDS] ")
                .append(getBaseAddress() != null ? getBaseAddress().getHost() : "???")
                .append((getBaseAddress() != null && getBaseAddress().getPort() > 0) ? ":" + getBaseAddress().getPort() : "")
                .append(" - ")
                .append(treeViewTab != null ? treeViewTab : "???")
                .append(filter != null ? filter : "")
                .append(" (")
                .append(zoneId != null ? zoneId : "???")
                .append(")").toString();
    }

    @Override
    public Map getParams() {
        Map params = new HashMap<>(super.getParams());
        params.put(ZONE_ID_PARAM_NAME, zoneId.toString());
        params.put(ENCODING_PARAM_NAME, encoding);
        params.put(TREE_VIEW_TAB_PARAM_NAME, treeViewTab.name());
        params.put(JRDS_FILTER, this.filter);
        return params;
    }

    @Override
    public void loadParams(Map params) throws DataAdapterException {
        if (params == null) {
            throw new InvalidAdapterParameterException("Could not find parameter list for adapter " + getSourceName());
        }
        super.loadParams(params);
        encoding = validateParameterNullity(params, ENCODING_PARAM_NAME);
        zoneId = validateParameter(params, ZONE_ID_PARAM_NAME,
                s -> {
                    if (s == null) {
                        throw new InvalidAdapterParameterException("Parameter " + ZONE_ID_PARAM_NAME + " is missing in adapter " + getSourceName());
                    }
                    return ZoneId.of(s);
                });
        treeViewTab = validateParameter(params, TREE_VIEW_TAB_PARAM_NAME, s -> s == null ? JrdsTreeViewTab.valueOf(params.get(TREE_VIEW_TAB_PARAM_NAME)) : JrdsTreeViewTab.HOSTS_TAB);
        this.filter = params.get(JRDS_FILTER);
        this.decoder = decoderFactory(zoneId);
    }

    @Override
    public String getEncoding() {
        return encoding;
    }

    @Override
    public ZoneId getTimeZoneId() {
        return zoneId;
    }

    @Override
    public CsvDecoder getDecoder() {
        return decoder;
    }

    @Override
    public void close() {
        super.close();
    }

    //endregion

    /**
     * Returns a collection of filters provided by the JRDS server.
     *
     * @return a collection of filters provided by the JRDS server.
     * @throws DataAdapterException if an error occurs while parsing the server's response.
     * @throws URISyntaxException   if the generated url is not valid.
     */
    public Collection discoverFilters() throws DataAdapterException, URISyntaxException {
        try {
            JsonJrdsTree t = gson.fromJson(getJsonTree(treeViewTab.getCommand(), treeViewTab.getArgument()), JsonJrdsTree.class);
            return Arrays.stream(t.items).filter(jsonJrdsItem -> JRDS_FILTER.equals(jsonJrdsItem.type)).map(i -> i.filter).collect(Collectors.toList());
        } catch (JsonParseException e) {
            throw new DataAdapterException("An error occurred while parsing the json response to getBindingTree request", e);
        }
    }

    private void attachNode(FilterableTreeItem tree, String id, Map nodes) throws DataAdapterException {
        JsonJrdsItem n = nodes.get(id);
        String currentPath = normalizeId(n.id);
        FilterableTreeItem newBranch = new FilterableTreeItem<>(
                new JrdsBindingBuilder()
                        .withParent(tree.getValue())
                        .withLabel(n.name)
                        .withPath(currentPath)
                        .withAdapter(this)
                        .build());
        if (JRDS_FILTER.equals(n.type)) {
            // add a dummy node so that the branch can be expanded
            newBranch.getInternalChildren().add(new FilterableTreeItem<>(null));
            // add a listener that will get the treeview filtered according to the selected filter/tag
            newBranch.expandedProperty().addListener(new FilteredViewListener(n, newBranch));
        } else {
            if (n.children != null) {
                for (JsonJrdsItem.JsonTreeRef ref : n.children) {
                    attachNode(newBranch, ref._reference, nodes);
                }
            } else {
                // add a dummy node so that the branch can be expanded
                newBranch.getInternalChildren().add(new FilterableTreeItem<>(null));
                // add a listener so that bindings for individual datastore are added lazily to avoid
                // dozens of individual call to "graphdesc" when the tree is built.
                newBranch.expandedProperty().addListener(new GraphDescListener(currentPath, newBranch, tree));
            }
        }
        tree.getInternalChildren().add(newBranch);
    }

    private String normalizeId(String id) {
        if (id == null || id.trim().length() == 0) {
            throw new IllegalArgumentException("Argument id cannot be null or blank");
        }
        String[] data = id.split("\\.");
        return data[data.length - 1];
    }

    private String getJsonTree(String tabName, String argName) throws DataAdapterException, URISyntaxException {
        return getJsonTree(tabName, argName, null);
    }

    private String getJsonTree(String tabName, String argName, String argValue) throws DataAdapterException, URISyntaxException {
        List params = new ArrayList<>();
        params.add(new BasicNameValuePair("tab", tabName));
        if (argName != null && argValue != null && argValue.trim().length() > 0) {
            params.add(new BasicNameValuePair(argName, argValue));
        }
        String entityString = doHttpGet(craftRequestUri("jsontree", params), response -> {
            var entity = response.getEntity();
            try {
                if ("application/json".equalsIgnoreCase(ContentType.getOrDefault(entity).getMimeType())) {
                    return EntityUtils.toString(entity);
                }
                logger.error("HTTP response content type is '" +
                        ContentType.getOrDefault(entity).getMimeType() +
                        " (expected 'application/json')");
                return null;
            } finally {
                EntityUtils.consumeQuietly(entity);
            }
        });
        logger.trace(entityString);
        return entityString;
    }


    private Graphdesc getGraphDescriptor(String id) throws DataAdapterException {
        URI requestUri = craftRequestUri("graphdesc", new BasicNameValuePair("id", id));

        return doHttpGet(requestUri, response -> {
            StatusLine statusLine = response.getStatusLine();
            if (statusLine.getStatusCode() == 404) {
                // This is probably an older version of JRDS that doesn't provide the graphdesc service,
                // so we're falling back to recovering the datastore name from the csv file provided by
                // the download service.
                logger.warn("Cannot found graphdesc service; falling back to legacy mode.");
                try {
                    return getGraphDescriptorLegacy(id);
                } catch (Exception e) {
                    throw new IOException("", e);
                }
            }
            HttpEntity entity = response.getEntity();
            if (statusLine.getStatusCode() >= 300) {
                EntityUtils.consume(entity);
                throw new HttpResponseException(statusLine.getStatusCode(),
                        statusLine.getReasonPhrase());
            }
            if (entity != null) {
                try {
                    return JAXB.unmarshal(XmlUtils.toNonValidatingSAXSource(entity.getContent()), Graphdesc.class);
                } catch (Exception e) {
                    throw new IOException("Failed to unmarshall graphdesc response", e);
                }
            }
            return null;
        });
    }

    private Graphdesc getGraphDescriptorLegacy(String id) throws DataAdapterException {
        Instant now = ZonedDateTime.now().toInstant();
        try (InputStream in = fetchRawData(id, now.minusSeconds(300), now, false)) {
            List headers = getDecoder().getDataColumnHeaders(in);
            Graphdesc desc = new Graphdesc();
            desc.seriesDescList = new ArrayList<>();
            for (String header : headers) {
                Graphdesc.SeriesDesc d = new Graphdesc.SeriesDesc();
                d.name = header;
                desc.seriesDescList.add(d);
            }
            return desc;
        } catch (IOException e) {
            throw new FetchingDataFromAdapterException(e);
        }
    }

    private CsvDecoder decoderFactory(ZoneId zoneId) {
        return new CsvDecoder(getEncoding(), DELIMITER,
                DoubleTimeSeriesProcessor::new,
                s -> ZonedDateTime.parse(s, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss").withZone(zoneId)));
    }

    private class GraphDescListener implements ChangeListener {
        private final String currentPath;
        private final FilterableTreeItem newBranch;
        private final FilterableTreeItem tree;

        public GraphDescListener(String currentPath, FilterableTreeItem newBranch, FilterableTreeItem tree) {
            this.currentPath = currentPath;
            this.newBranch = newBranch;
            this.tree = tree;
        }

        @Override
        public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) {
            if (newValue) {
                try {
                    Graphdesc graphdesc = getGraphDescriptor(currentPath);
                    newBranch.setValue(new JrdsBindingBuilder()
                            .withGraphDesc(graphdesc)
                            .withParent(tree.getValue())
                            .withLegend(newBranch.getValue().getLegend())
                            .withPath(currentPath)
                            .withAdapter(JrdsDataAdapter.this)
                            .build());
                    for (int i = 0; i < graphdesc.seriesDescList.size(); i++) {
                        String graphType = graphdesc.seriesDescList.get(i).graphType;
                        if (!"none".equalsIgnoreCase(graphType) && !"comment".equalsIgnoreCase(graphType)) {
                            newBranch.getInternalChildren().add(new FilterableTreeItem<>((new JrdsBindingBuilder()
                                    .withGraphDesc(graphdesc, i)
                                    .withParent(newBranch.getValue())
                                    .withPath(currentPath)
                                    .withAdapter(JrdsDataAdapter.this)
                                    .build())));
                        }
                    }
                    //remove dummy node
                    newBranch.getInternalChildren().remove(0);
                    // remove the listener so it isn't executed next time node is expanded
                    newBranch.expandedProperty().removeListener(this);
                } catch (Exception e) {
                    Dialogs.notifyException("Failed to retrieve graph description", e);
                }
            }
        }
    }

    private class FilteredViewListener implements ChangeListener {
        private final JsonJrdsItem n;
        private final FilterableTreeItem newBranch;

        public FilteredViewListener(JsonJrdsItem n, FilterableTreeItem newBranch) {
            this.n = n;
            this.newBranch = newBranch;
        }

        @Override
        public void changed(ObservableValue observable, Boolean oldValue, Boolean newValue) {
            if (newValue) {
                try {
                    JsonJrdsTree t = gson.fromJson(getJsonTree(treeViewTab.getCommand(), JRDS_FILTER, n.name), JsonJrdsTree.class);
                    Map m = Arrays.stream(t.items).collect(Collectors.toMap(o -> o.id, (o -> o)));
                    for (JsonJrdsItem branch : Arrays.stream(t.items).filter(jsonJrdsItem -> JRDS_TREE.equals(jsonJrdsItem.type) || JRDS_FILTER.equals(jsonJrdsItem.type)).collect(Collectors.toList())) {
                        attachNode(newBranch, branch.id, m);
                    }
                    //remove dummy node
                    newBranch.getInternalChildren().remove(0);
                    // remove the listener so it isn't executed next time node is expanded
                    newBranch.expandedProperty().removeListener(this);
                } catch (Exception e) {
                    Dialogs.notifyException("Failed to retrieve graph description", e);
                }
            }
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy