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

org.yamcs.parameter.ParameterWithIdRequestHelper Maven / Gradle / Ivy

There is a newer version: 5.10.9
Show newest version
package org.yamcs.parameter;

import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.ScheduledThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.yamcs.InvalidIdentification;
import org.yamcs.InvalidRequestIdentification;
import org.yamcs.NoPermissionException;
import org.yamcs.protobuf.Pvalue.AcquisitionStatus;
import org.yamcs.protobuf.Yamcs.NamedObjectId;
import org.yamcs.security.ObjectPrivilegeType;
import org.yamcs.security.User;
import org.yamcs.utils.AggregateUtil;
import org.yamcs.utils.StringConverter;
import org.yamcs.xtce.Parameter;
import org.yamcs.xtce.PathElement;

import com.google.common.collect.ArrayListMultimap;
import com.google.common.collect.ListMultimap;

/**
 * This sits in front of the ParameterRequestManager and implements subscriptions based on NamedObjectId taking care to
 * send to the consumers the parameters with the requested id.
 * 
 * A client can request in fact the same parameter with two different names and they will get it twice each time.
 * 
 * In addition it can also provide updates on parameter expirations.
 * 
 * TODO: impose some subscription limits
 * 
 * @author nm
 *
 */
public class ParameterWithIdRequestHelper implements ParameterConsumer {
    ParameterRequestManager prm;
    final ParameterWithIdConsumer listener;
    static Logger log = LoggerFactory.getLogger(ParameterWithIdRequestHelper.class.getName());
    Map subscriptions = new ConcurrentHashMap<>();

    // how often to check expiration
    private static long CHECK_EXPIRATION_INTERVAL = 1000;
    final static ScheduledThreadPoolExecutor timer = new ScheduledThreadPoolExecutor(1);
    int subscribeAllId = -1;

    public ParameterWithIdRequestHelper(ParameterRequestManager prm, ParameterWithIdConsumer listener) {
        this.prm = prm;
        this.listener = listener;
        schedulePeriodicExpirationChecking(this);
    }

    public int addRequest(List idList, User user)
            throws InvalidIdentification, NoPermissionException {
        return addRequest(idList, false, user);
    }

    public int addRequest(List idList, boolean checkExpiration, User user)
            throws InvalidIdentification, NoPermissionException {
        List plist = checkNames(idList);
        Subscription subscr = new Subscription(checkExpiration);
        for (int i = 0; i < idList.size(); i++) {
            checkParameterPrivilege(user, plist.get(i).p.getQualifiedName());
            subscr.add(plist.get(i));
        }
        int subscriptionId = prm.addRequest(plist.stream().map(pwid -> pwid.p).collect(Collectors.toList()), this);
        subscriptions.put(subscriptionId, subscr);

        return subscriptionId;
    }

    public void addItemsToRequest(int subscriptionId, List idList, User user)
            throws InvalidIdentification, NoPermissionException {
        Subscription subscr = subscriptions.get(subscriptionId);
        if (subscr == null) {
            log.warn("add item requested for an invalid subscription id {}", subscriptionId);
            throw new InvalidRequestIdentification("Invalid subcription id", subscriptionId);
        }
        List plist = checkNames(idList);
        synchronized (subscr) {
            for (int i = 0; i < idList.size(); i++) {
                Parameter p = plist.get(i).p;
                checkParameterPrivilege(user, p.getQualifiedName());
                NamedObjectId id = idList.get(i);
                if (!subscr.add(plist.get(i))) {
                    log.info("Ignoring duplicate subscription for '{}', id: {}", p.getName(),
                            StringConverter.idToString(id));
                }
            }
        }
        prm.addItemsToRequest(subscriptionId, plist.stream().map(pwid -> pwid.p).collect(Collectors.toList()));
    }

    private static void schedulePeriodicExpirationChecking(ParameterWithIdRequestHelper x) {
        // trick to allow GC to collect this object and remove it from the timer
        final WeakReference ref = new WeakReference<>(x);
        final AtomicReference> futureRef = new AtomicReference<>();
        ScheduledFuture future = timer.scheduleAtFixedRate(() -> {
            ParameterWithIdRequestHelper pwirh = ref.get();
            if (pwirh == null) {
                ScheduledFuture f = futureRef.get();
                f.cancel(false);
            } else {
                pwirh.checkPeriodicExpiration();
            }
        }, CHECK_EXPIRATION_INTERVAL, CHECK_EXPIRATION_INTERVAL, TimeUnit.MILLISECONDS);
        futureRef.set(future);
    }

    List checkNames(List idList) throws InvalidIdentification {
        return checkNames(prm, idList);
    }

    // turn NamedObjectId to Parameter references
    public static ParameterWithId checkName(ParameterRequestManager prm, NamedObjectId id)
            throws InvalidIdentification {
        String name = id.getName();
        int x = AggregateUtil.findSeparator(name);
        NamedObjectId id1;
        PathElement[] path;
        if (x > 0) { // this is an array or aggregate element
            id1 = NamedObjectId.newBuilder(id).setName(name.substring(0, x)).build();
            try {
                path = AggregateUtil.parseReference(name.substring(x));
            } catch (IllegalArgumentException e) {
                throw new InvalidIdentification(id);
            }
        } else {
            id1 = id;
            path = null;
        }
        Parameter p = prm.getParameter(id1);
        if (path != null) {
            if (!AggregateUtil.verifyPath(p.getParameterType(), path)) {
                throw new InvalidIdentification(id);
            }
        }
        return new ParameterWithId(p, id, path);

    }

    // turn NamedObjectId to Parameter references
    public static List checkNames(ParameterRequestManager prm, List idList)
            throws InvalidIdentification {
        List result = new ArrayList<>();
        List invalid = new ArrayList<>(0);
        for (NamedObjectId id : idList) {
            try {
                result.add(checkName(prm, id));
            } catch (InvalidIdentification e) {
                invalid.add(id);
                continue;
            }

        }
        if (!invalid.isEmpty()) {
            log.info("Throwing invalid identification for the following items: {}", invalid);
            throw new InvalidIdentification(invalid);
        }
        return result;
    }

    public void removeRequest(int subscriptionId) {
        if (subscriptions.remove(subscriptionId) == null) {
            log.warn("remove requested for an invalid subscription id {}", subscriptionId);
            return;
        }
        prm.removeRequest(subscriptionId);
    }

    public void removeItemsFromRequest(int subscriptionId, List parameterIds, User user)
            throws NoPermissionException {
        Subscription subscr = subscriptions.get(subscriptionId);

        if (subscr == null) {
            log.warn("remove requested for an invalid subscription id {}", subscriptionId);
            return;
        }
        List paramsToRemove = new ArrayList<>();
        synchronized (subscr) {
            for (NamedObjectId id : parameterIds) {
                Parameter p = subscr.remove(id);
                if (p != null) {
                    paramsToRemove.add(p);
                }
            }
        }
        if (!paramsToRemove.isEmpty()) {
            prm.removeItemsFromRequest(subscriptionId, paramsToRemove);
        }
    }

    public ParameterRequestManager getPrm() {
        return prm;
    }

    public int subscribeAll(User user) throws NoPermissionException {
        checkParameterPrivilege(user, ".*");
        subscribeAllId = prm.subscribeAll(this);
        return subscribeAllId;
    }

    /**
     * retrieve the subscribed values from cache
     * 
     * @param subscriptionId
     * @return
     */
    public List getValuesFromCache(int subscriptionId) {
        Subscription subscr = subscriptions.get(subscriptionId);
        if (subscr == null) {
            log.warn("add item requested for an invalid subscription id {}", subscriptionId);
            throw new InvalidRequestIdentification("Invalid subcription id", subscriptionId);
        }
        long now = prm.processor.getCurrentTime();

        List values = prm.getValuesFromCache(subscr.params.keySet());
        List pvlist = new ArrayList<>(values.size());
        for (ParameterValue pv : values) {
            if (pv.isExpired(now)) {
                pv = new ParameterValue(pv);
                pv.setAcquisitionStatus(AcquisitionStatus.EXPIRED);
            }
            if (subscr.checkExpiration && pv.hasExpirationTime()) {
                subscr.pvexp.put(pv.getParameter(), pv);
            }
            List l = subscr.params.get(pv.getParameter());
            if (l == null) {
                log.warn("Received values for a parameter not requested: {}", pv.getParameter());
                continue;
            }
            addValueForAllIds(pvlist, l, pv);
        }

        return pvlist;
    }

    /**
     * Retrieve a list of parameter values from cache. This call does not block.
     * 
     * @param idList
     * @param user
     * @return
     * @throws InvalidIdentification
     * @throws NoPermissionException
     */
    public List getValuesFromCache(List idList, User user)
            throws InvalidIdentification, NoPermissionException {
        List plist = checkNames(idList);

        ListMultimap lm = ArrayListMultimap.create();
        for (int i = 0; i < idList.size(); i++) {
            ParameterWithId pwid = plist.get(i);
            checkParameterPrivilege(user, pwid.p.getQualifiedName());
            lm.put(pwid.p, pwid);
        }

        List values = prm
                .getValuesFromCache(plist.stream().map(pwid -> pwid.p).distinct().collect(Collectors.toList()));
        List pvlist = new ArrayList<>(values.size());

        for (ParameterValue pv : values) {
            List l = lm.get(pv.getParameter());
            if (l == null) {
                log.warn("Received values for a parameter not requested: {}", pv.getParameter());
                continue;
            }
            addValueForAllIds(pvlist, l, pv);
        }

        return pvlist;
    }

    // adds the pv into plist with all ids from idList
    private void addValueForAllIds(List plist, List idList, ParameterValue pv) {
        for (ParameterWithId pwid : idList) {
            ParameterValue pv1 = null;
            if (pwid.path != null) {
                try {
                    pv1 = AggregateUtil.extractMember(pv, pwid.path);
                    if (pv1 == null) { // could be that we reference an element of an array that doesn't exist
                        continue;
                    }
                } catch (Exception e) {
                    log.error("Failed to extract {} from parameter value {}", Arrays.toString(pwid.path), pv, e);
                    continue;
                }
            } else {
                pv1 = pv;
            }

            ParameterValueWithId pvwi = new ParameterValueWithId(pv1, pwid.id);
            plist.add(pvwi);
        }
    }

    /**
     * Called from {@link ParameterRequestManager when new parameters are available to be sent to clients}
     */
    @Override
    public void updateItems(int subscriptionId, List items) {
        if (subscriptionId == subscribeAllId) {
            updateAllSubscription(subscriptionId, items);
            return;
        }
        Subscription subscription = subscriptions.get(subscriptionId);
        if (subscription == null) { // probably the subscription has just been removed
            log.debug("Received an updateItems for an unknown subscription {}", subscriptionId);
            return;
        }

        List plist = new ArrayList<>(items.size());
        synchronized (subscription) {
            if (subscription.checkExpiration) {
                long now = getAquisitionTime(items);

                List expired = updateAndCheckExpiration(subscription, items, now);
                if (!expired.isEmpty()) {
                    log.debug("Updating {} parameters due to expiration", expired.size());
                    listener.update(subscriptionId, expired);
                }
            }

            for (ParameterValue pv : items) {
                addValueForAllSubscribedIds(plist, subscription, pv);
            }
        }
        listener.update(subscriptionId, plist);
    }

    private void updateAllSubscription(int subscriptionId, List items) {
        List plist = new ArrayList<>(items.size());
        for (ParameterValue pv : items) {
            plist.add(new ParameterValueWithId(pv,
                    NamedObjectId.newBuilder().setName(pv.getParameterQualifiedName()).build()));
        }
        listener.update(subscriptionId, plist);
    }

    public void unselectPrm() {
        for (int subscriptionId : subscriptions.keySet()) {
            prm.removeRequest(subscriptionId);
        }
        prm = null;
    }

    public List selectPrm(ParameterRequestManager prm, User user) throws NoPermissionException {
        List invalid = new ArrayList<>();
        // Parameter references may be invalid for the new processor
        // we have to re-create the subscriptions starting from the original subscribed names
        // and take care that some names may have become invalid
        this.prm = prm;
        for (int subscriptionId : subscriptions.keySet()) {
            Subscription subscr = subscriptions.get(subscriptionId);
            synchronized (subscr) {
                List idList = subscr.getallIds();
                List plist;
                try {
                    plist = checkNames(idList);
                } catch (InvalidIdentification e) {
                    log.warn("Got invalid identification when selecting parameters for processor {}: {}",
                            prm.processor.getName(), e.getInvalidParameters());
                    idList.removeAll(e.getInvalidParameters());
                    invalid.addAll(e.getInvalidParameters());
                    try {
                        plist = checkNames(idList);
                    } catch (InvalidIdentification e1) { // shouldn't happen again
                        throw new IllegalStateException(e1);
                    }
                }
                assert (idList.size() == plist.size());
                Subscription subscr1 = new Subscription(subscr.checkExpiration);

                for (int i = 0; i < plist.size(); i++) {
                    ParameterWithId pwid = plist.get(i);
                    checkParameterPrivilege(user, pwid.p.getQualifiedName());
                    subscr1.add(pwid);
                }
                prm.addRequest(subscriptionId, plist.stream().map(pwid -> pwid.p).collect(Collectors.toList()), this);
                subscriptions.put(subscriptionId, subscr1);
            }
        }
        return invalid;
    }

    /**
     * Change processor and return the list of parameters that were valid in the old processor and are not anymore
     */
    public List switchPrm(ParameterRequestManager newPrm, User user) throws NoPermissionException {
        if (prm != null) {
            unselectPrm();
        }
        if (newPrm != null) {
            return selectPrm(newPrm, user);
        } else {
            return Collections.emptyList();
        }
    }

    private long getAquisitionTime(List items) {
        for (ParameterValue pv : items) {
            if (pv.hasAcquisitionTime()) {
                return pv.getAcquisitionTime();
            }
        }
        return prm.processor.getCurrentTime();
    }

    // adds the pv into plist with all ids subscribed
    private void addValueForAllSubscribedIds(List plist, Subscription subscription,
            ParameterValue pv) {
        Parameter p = pv.getParameter();
        List idList = subscription.get(p);
        if (idList == null || idList.isEmpty()) {
            log.warn("Received values for a parameter not subscribed: {}", pv.getParameter());
            return;
        }
        addValueForAllIds(plist, idList, pv);
    }

    private void checkPeriodicExpiration() {
        for (Map.Entry me : subscriptions.entrySet()) {
            Subscription subscription = me.getValue();
            synchronized (subscription) {
                long now = prm.processor.getCurrentTime();
                if ((subscription.checkExpiration)
                        && (now - subscription.lastExpirationCheck > CHECK_EXPIRATION_INTERVAL)) {
                    List expired = checkExpiration(subscription, now);
                    if (!expired.isEmpty()) {
                        log.debug("Updating {} parameters due to expiration", expired.size());
                        listener.update(me.getKey(), expired);
                    }
                }
            }
        }
    }

    // update the expiration list with new values and check expiration of parameters that are just updating
    // in case the expiration is shorter than the check interval - this method would detect and send the parameters that
    // have just expired
    private List updateAndCheckExpiration(Subscription subscription, List items,
            long now) {
        List expired = new ArrayList<>();
        for (ParameterValue pv : items) {
            Parameter p = pv.getParameter();
            ParameterValue oldPv;
            if (pv.hasExpirationTime()) {
                oldPv = subscription.pvexp.put(p, pv);
            } else {
                oldPv = subscription.pvexp.remove(p);
            }
            if ((oldPv != null) && oldPv.getAcquisitionStatus() == AcquisitionStatus.ACQUIRED && oldPv.isExpired(now)) {
                ParameterValue tmp = new ParameterValue(oldPv); // make a copy because this is shared by other
                                                                // subscribers
                tmp.setAcquisitionStatus(AcquisitionStatus.EXPIRED);
                addValueForAllSubscribedIds(expired, subscription, tmp);
            }
        }
        return expired;
    }

    // check expiration of all parameters from subscription
    private List checkExpiration(Subscription subscription, long now) {
        List expired = new ArrayList<>();
        for (ParameterValue pv : subscription.pvexp.values()) {
            if (pv.getAcquisitionStatus() == AcquisitionStatus.ACQUIRED && pv.isExpired(now)) {

                ParameterValue tmp = new ParameterValue(pv); // make a copy because this is shared by other subscribers
                tmp.setAcquisitionStatus(AcquisitionStatus.EXPIRED);
                subscription.pvexp.put(pv.getParameter(), tmp);
                addValueForAllSubscribedIds(expired, subscription, tmp);
            }
        }
        subscription.lastExpirationCheck = now;
        return expired;
    }

    /**
     * Check if the user has a privilege for the specified parameter name
     * 
     * @param authToken
     * @param parameterName
     * @throws NoPermissionException
     */
    private void checkParameterPrivilege(User user, String parameterName)
            throws NoPermissionException {
        if (!user.hasObjectPrivilege(ObjectPrivilegeType.ReadParameter, parameterName)) {
            throw new NoPermissionException("User " + user + " has no permission for parameter " + parameterName);
        }
    }

    public void quit() {
        for (int subscriptionId : subscriptions.keySet()) {
            prm.removeRequest(subscriptionId);
        }
        subscriptions.clear();
        if (subscribeAllId != -1) {
            prm.unsubscribeAll(subscribeAllId);
        }
    }

    static class Subscription {
        Map> params = new LinkedHashMap<>();
        boolean checkExpiration = false;
        long lastExpirationCheck = -1;
        // contains the parameters that have an expiration time set
        Map pvexp;

        public Subscription(boolean checkExpiration) {
            this.checkExpiration = checkExpiration;
            if (checkExpiration) {
                pvexp = new HashMap<>();
            }
        }

        public List getallIds() {
            List r = new ArrayList<>();
            for (List l : params.values()) {
                for (ParameterWithId pwid : l) {
                    r.add(pwid.id);
                }
            }
            return r;
        }

        /**
         * looks and removes the id from a list and returns the associated parameter if there is no id mapped to it
         * anymore otherwise return null
         * 
         * @param id
         * @return
         */
        public Parameter remove(NamedObjectId id) {
            Parameter p = null;
            boolean found = false;
            for (Map.Entry> me : params.entrySet()) {
                List l = me.getValue();
                for (ParameterWithId pwid : l) {
                    if (pwid.id.equals(id)) {
                        l.remove(pwid);
                        found = true;
                        break;
                    }
                }
                if (found) {
                    if (l.isEmpty()) {
                        p = me.getKey();
                    }
                    break;
                }
            }
            if (p != null) {
                params.remove(p);
                if (pvexp != null) {
                    pvexp.remove(p);
                }
            }
            return p;
        }

        public List get(Parameter parameter) {
            return params.get(parameter);
        }

        public boolean add(ParameterWithId pwid) {
            List l = params.get(pwid.p);
            if (l == null) {
                l = new ArrayList<>();
                params.put(pwid.p, l);
            } else if (l.stream().anyMatch(pwid1 -> pwid1.id.equals(pwid.id))) {
                return false;
            }
            l.add(pwid);

            return true;
        }
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy