package org.yamcs.web.rest.processor; import java.util.ArrayList; import java.util.HashSet; import java.util.LinkedHashSet; import java.util.List; import java.util.Set; import java.util.regex.Pattern; import org.yamcs.Processor; import org.yamcs.YamcsException; import org.yamcs.management.ManagementGpbHelper; import org.yamcs.management.ManagementService; import org.yamcs.protobuf.Rest.CreateProcessorRequest; import org.yamcs.protobuf.Rest.EditProcessorRequest; import org.yamcs.protobuf.Rest.ListClientsResponse; import org.yamcs.protobuf.Rest.ListProcessorsResponse; import org.yamcs.protobuf.SchemaRest; import org.yamcs.protobuf.SchemaYamcsManagement; import org.yamcs.protobuf.Yamcs.CommandHistoryReplayRequest; import org.yamcs.protobuf.Yamcs.EndAction; import org.yamcs.protobuf.Yamcs.NamedObjectId; import org.yamcs.protobuf.Yamcs.PacketReplayRequest; import org.yamcs.protobuf.Yamcs.ParameterReplayRequest; import org.yamcs.protobuf.Yamcs.PpReplayRequest; import org.yamcs.protobuf.Yamcs.ReplayRequest; import org.yamcs.protobuf.Yamcs.ReplaySpeed; import org.yamcs.protobuf.Yamcs.ReplaySpeed.ReplaySpeedType; import org.yamcs.protobuf.YamcsManagement.ClientInfo; import org.yamcs.protobuf.YamcsManagement.ClientInfo.ClientState; import org.yamcs.protobuf.YamcsManagement.ProcessorInfo; import org.yamcs.protobuf.YamcsManagement.ProcessorManagementRequest; import org.yamcs.utils.TimeEncoding; import org.yamcs.web.BadRequestException; import org.yamcs.web.HttpException; import org.yamcs.web.rest.RestHandler; import org.yamcs.web.rest.RestRequest; import org.yamcs.web.rest.RestRequest.Option; import org.yamcs.web.rest.Route; import org.yamcs.xtce.Parameter; import org.yamcs.xtce.XtceDb; import org.yamcs.xtceproc.XtceDbFactory; public class ProcessorRestHandler extends RestHandler { @Route(path = "/api/processors/:instance/:processor/clients", method = "GET") public void listClientsForProcessor(RestRequest req) throws HttpException { Processor processor = verifyProcessor(req, req.getRouteParam("instance"), req.getRouteParam("processor")); Set<ClientInfo> clients = ManagementService.getInstance().getClientInfo(); ListClientsResponse.Builder responseb = ListClientsResponse.newBuilder(); for (ClientInfo client : clients) { if (processor.getInstance().equals(client.getInstance()) && processor.getName().equals(client.getProcessorName())) { responseb.addClient(ClientInfo.newBuilder(client).setState(ClientState.CONNECTED)); } } completeOK(req, responseb.build(), SchemaRest.ListClientsResponse.WRITE); } @Route(path = "/api/processors", method = "GET") public void listProcessors(RestRequest req) throws HttpException { ListProcessorsResponse.Builder response = ListProcessorsResponse.newBuilder(); for (Processor processor : Processor.getProcessors()) { response.addProcessor(toProcessorInfo(processor, req, true)); } completeOK(req, response.build(), SchemaRest.ListProcessorsResponse.WRITE); } @Route(path = "/api/processors/:instance", method = "GET") public void listProcessorsForInstance(RestRequest req) throws HttpException { String instance = verifyInstance(req, req.getRouteParam("instance")); ListProcessorsResponse.Builder response = ListProcessorsResponse.newBuilder(); for (Processor processor : Processor.getProcessors(instance)) { response.addProcessor(toProcessorInfo(processor, req, true)); } completeOK(req, response.build(), SchemaRest.ListProcessorsResponse.WRITE); } @Route(path = "/api/processors/:instance/:processor", method = "GET") public void getProcessor(RestRequest req) throws HttpException { Processor processor = verifyProcessor(req, req.getRouteParam("instance"), req.getRouteParam("processor")); ProcessorInfo pinfo = toProcessorInfo(processor, req, true); completeOK(req, pinfo, SchemaYamcsManagement.ProcessorInfo.WRITE); } @Route(path = "/api/processors/:instance/:processor", method = { "PATCH", "PUT", "POST" }) public void editProcessor(RestRequest req) throws HttpException { Processor processor = verifyProcessor(req, req.getRouteParam("instance"), req.getRouteParam("processor")); if (!processor.isReplay()) { throw new BadRequestException("Cannot update a non-replay processor"); } EditProcessorRequest request = req.bodyAsMessage(SchemaRest.EditProcessorRequest.MERGE).build(); String newState = null; if (request.hasState()) { newState = request.getState(); } if (req.hasQueryParameter("state")) { newState = req.getQueryParameter("state"); } if (newState != null) { switch (newState.toLowerCase()) { case "running": processor.resume(); break; case "paused": processor.pause(); break; } } long seek = TimeEncoding.INVALID_INSTANT; if (request.hasSeek()) { seek = RestRequest.parseTime(request.getSeek()); } if (req.hasQueryParameter("seek")) { seek = req.getQueryParameterAsDate("seek"); } if (seek != TimeEncoding.INVALID_INSTANT) { processor.seek(seek); } String speed = null; if (request.hasSpeed()) { speed = request.getSpeed().toLowerCase(); } if (req.hasQueryParameter("speed")) { speed = req.getQueryParameter("speed").toLowerCase(); } if (speed != null) { ReplaySpeed replaySpeed; if ("afap".equals(speed)) { replaySpeed = ReplaySpeed.newBuilder().setType(ReplaySpeedType.AFAP).build(); } else if (speed.endsWith("x")) { try { float factor = Float.parseFloat(speed.substring(0, speed.length() - 1)); replaySpeed = ReplaySpeed.newBuilder() .setType(ReplaySpeedType.REALTIME) .setParam(factor).build(); } catch (NumberFormatException e) { throw new BadRequestException("Speed factor is not a valid number"); } } else { try { int fixedDelay = Integer.parseInt(speed); replaySpeed = ReplaySpeed.newBuilder() .setType(ReplaySpeedType.FIXED_DELAY) .setParam(fixedDelay).build(); } catch (NumberFormatException e) { throw new BadRequestException("Fixed delay value is not an integer"); } } processor.changeSpeed(replaySpeed); } completeOK(req); } @Route(path = "/api/processors/:instance", method = "POST") public void createProcessorForInstance(RestRequest req) throws HttpException { CreateProcessorRequest request = req.bodyAsMessage(SchemaRest.CreateProcessorRequest.MERGE).build(); if(request.hasStart()) { //the old create processor only allowed creating archive processors createProcessorForInstanceOld(req, request); return; } //the new one just passes on the config to the processor factory String yamcsInstance = verifyInstance(req, req.getRouteParam("instance")); String processorName; if (request.hasName()) { processorName = request.getName(); } else if (req.hasQueryParameter("name")) { processorName = req.getQueryParameter("name"); } else { throw new BadRequestException("No processor name was specified"); } String processorType = null; if (request.hasType()) { processorType = request.getType(); } else if (req.hasQueryParameter("type")) { processorName = req.getQueryParameter("type"); } else { throw new BadRequestException("No processor type was specified"); } ProcessorManagementRequest.Builder reqb = ProcessorManagementRequest.newBuilder(); reqb.setInstance(yamcsInstance); reqb.setName(processorName); reqb.setType(processorType); if(request.hasPersistent()) { reqb.setPersistent(request.getPersistent()); } reqb.addAllClientId(request.getClientIdList()); if(request.hasConfig()) { reqb.setConfig(request.getConfig()); } ManagementService mservice = ManagementService.getInstance(); try { mservice.createProcessor(reqb.build(), req.getAuthToken()); completeOK(req); } catch (YamcsException e) { throw new BadRequestException(e.getMessage()); } } public void createProcessorForInstanceOld(RestRequest req, CreateProcessorRequest request) throws HttpException { String instance = verifyInstance(req, req.getRouteParam("instance")); XtceDb mdb = XtceDbFactory.getInstance(instance); String name = null; long start = TimeEncoding.INVALID_INSTANT; long stop = TimeEncoding.INVALID_INSTANT; String type = "Archive"; // API currently only supports standard replays String speed = "1x"; boolean loop = false; boolean persistent = false; Set<Integer> clientIds = new HashSet<>(); List<String> paraPatterns = new ArrayList<>(); List<String> paraGroups = new ArrayList<>(); List<String> packetNames = new ArrayList<>(); boolean cmdhist = false; if (request.hasName()) { name = request.getName(); } if (request.hasStart()) { start = RestRequest.parseTime(request.getStart()); } if (request.hasStop()) { stop = RestRequest.parseTime(request.getStop()); } if (request.hasSpeed()) { speed = request.getSpeed().toLowerCase(); } if (request.hasLoop()) { loop = request.getLoop(); } if (request.hasCmdhist()) { cmdhist = request.getCmdhist(); } if (request.hasPersistent()) { persistent = request.getPersistent(); } clientIds.addAll(request.getClientIdList()); paraPatterns.addAll(request.getParanameList()); paraGroups.addAll(request.getPpgroupList()); packetNames.addAll(request.getPacketnameList()); // Query params get priority if (req.hasQueryParameter("name")) { name = req.getQueryParameter("name"); } if (req.hasQueryParameter("start")) { start = req.getQueryParameterAsDate("start"); } if (req.hasQueryParameter("stop")) { stop = req.getQueryParameterAsDate("stop"); } if (req.hasQueryParameter("speed")) { speed = req.getQueryParameter("speed").toLowerCase(); } if (req.hasQueryParameter("loop")) { loop = req.getQueryParameterAsBoolean("loop"); } if (req.hasQueryParameter("paraname")) { paraPatterns.addAll(req.getQueryParameterList("paraname")); } if (req.hasQueryParameter("ppgroup")) { paraGroups.addAll(req.getQueryParameterList("ppgroup")); } if (req.hasQueryParameter("paragroup")) { paraGroups.addAll(req.getQueryParameterList("paragroup")); } if (req.hasQueryParameter("packetname")) { packetNames.addAll(req.getQueryParameterList("packetname")); } if (req.hasQueryParameter("cmdhist")) { cmdhist = req.getQueryParameterAsBoolean("cmdhist"); } if (req.hasQueryParameter("persistent")) { persistent = req.getQueryParameterAsBoolean("persistent"); } if (req.hasQueryParameter("clientId")) { clientIds.addAll(request.getClientIdList()); } // Only these must be user-provided if (name == null) { throw new BadRequestException("No processor name was specified"); } if (start == TimeEncoding.INVALID_INSTANT) { throw new BadRequestException("No start time was specified"); } // Make internal processor request ProcessorManagementRequest.Builder reqb = ProcessorManagementRequest.newBuilder(); reqb.setInstance(instance); reqb.setName(name); reqb.setType(type); reqb.setPersistent(persistent); reqb.addAllClientId(clientIds); ReplayRequest.Builder rrb = ReplayRequest.newBuilder(); rrb.setEndAction(loop ? EndAction.LOOP : EndAction.STOP); rrb.setStart(start); if (stop != TimeEncoding.INVALID_INSTANT) { rrb.setStop(stop); } // Replay Speed if ("afap".equals(speed)) { rrb.setSpeed(ReplaySpeed.newBuilder().setType(ReplaySpeedType.AFAP)); } else if (speed.endsWith("x")) { try { float factor = Float.parseFloat(speed.substring(0, speed.length() - 1)); rrb.setSpeed(ReplaySpeed.newBuilder() .setType(ReplaySpeedType.REALTIME) .setParam(factor)); } catch (NumberFormatException e) { throw new BadRequestException("Speed factor is not a valid number"); } } else { try { int fixedDelay = Integer.parseInt(speed); rrb.setSpeed(ReplaySpeed.newBuilder() .setType(ReplaySpeedType.FIXED_DELAY) .setParam(fixedDelay)); } catch (NumberFormatException e) { throw new BadRequestException("Fixed delay is not an integer"); } } // Resolve parameter inclusion patterns // IMO this should actually all be done by the replay server itself. Not just in REST. Set<NamedObjectId> includedParameters = new LinkedHashSet<>(); // Preserve order (in case it matters) for (String pattern : paraPatterns) { if (!pattern.startsWith("/")) { pattern = "/" + pattern; // only xtce } boolean resolved = false; // Is it a namespace? Include parameters directly at that level. for (String namespace : mdb.getNamespaces()) { if (pattern.equals(namespace)) { mdb.getSpaceSystem(namespace).getParameters().forEach(p -> { includedParameters.add(NamedObjectId.newBuilder().setName(p.getQualifiedName()).build()); }); resolved = true; break; } } if (resolved) { continue; } // Is it a parameter name? Do index lookup Parameter p = mdb.getParameter(pattern); if (p != null) { includedParameters.add(NamedObjectId.newBuilder().setName(p.getQualifiedName()).build()); continue; } // Now, the slow approach. Match on qualified names, with support for star-wildcards Pattern regex = Pattern.compile(pattern.replace("*", ".*")); for (Parameter para : mdb.getParameters()) { if (regex.matcher(para.getQualifiedName()).matches()) { includedParameters.add(NamedObjectId.newBuilder().setName(para.getQualifiedName()).build()); } } // Currently does not error when an invalid pattern is specified. Seems like the right thing now. } if (!includedParameters.isEmpty()) { rrb.setParameterRequest(ParameterReplayRequest.newBuilder().addAllNameFilter(includedParameters)); } // PP groups are just passed. Not sure if we should keep support for this. Parameters are not filterable // on containers either, so I don't see why these get special treatment. Would prefer they are handled // in the above paraPatterns loop instead. if (!paraGroups.isEmpty()) { rrb.setPpRequest(PpReplayRequest.newBuilder().addAllGroupNameFilter(paraGroups)); } // Packet names are also just passed. We may want to try something fancier here with wildcard support. if (!packetNames.isEmpty()) { if (packetNames.size() == 1 && packetNames.get(0).equals("*")) { rrb.setPacketRequest(PacketReplayRequest.newBuilder()); } else { List<NamedObjectId> packetIds = new ArrayList<>(); packetNames.forEach(packetName -> { packetIds.add(NamedObjectId.newBuilder().setName(packetName).build()); }); rrb.setPacketRequest(PacketReplayRequest.newBuilder().addAllNameFilter(packetIds)); } } if (cmdhist) { rrb.setCommandHistoryRequest(CommandHistoryReplayRequest.newBuilder()); } reqb.setReplaySpec(rrb); ManagementService mservice = ManagementService.getInstance(); try { mservice.createProcessor(reqb.build(), req.getAuthToken()); completeOK(req); } catch (YamcsException e) { throw new BadRequestException(e.getMessage()); } } public static ProcessorInfo toProcessorInfo(Processor processor, RestRequest req, boolean detail) { ProcessorInfo.Builder b; if (detail) { ProcessorInfo pinfo = ManagementGpbHelper.toProcessorInfo(processor); b = ProcessorInfo.newBuilder(pinfo); } else { b = ProcessorInfo.newBuilder().setName(processor.getName()); } String instance = processor.getInstance(); String name = processor.getName(); if (!req.getOptions().contains(Option.NO_LINK)) { String apiURL = req.getApiURL(); b.setUrl(apiURL + "/processors/" + instance + "/" + name); b.setClientsUrl(apiURL + "/processors/" + instance + "/" + name + "/clients"); b.setParametersUrl(apiURL + "/processors/" + instance + "/" + name + "/parameters{/namespace}{/name}"); b.setCommandsUrl(apiURL + "/processors/" + instance + "/" + name + "/commands{/namespace}{/name}"); b.setCommandQueuesUrl(apiURL + "/processors/" + instance + "/" + name + "/cqueues{/name}"); } return b.build(); } }