CPFParser.java

  1. /* Copyright 2002-2020 CS GROUP
  2.  * Licensed to CS GROUP (CS) under one or more
  3.  * contributor license agreements.  See the NOTICE file distributed with
  4.  * this work for additional information regarding copyright ownership.
  5.  * CS licenses this file to You under the Apache License, Version 2.0
  6.  * (the "License"); you may not use this file except in compliance with
  7.  * the License.  You may obtain a copy of the License at
  8.  *
  9.  *   http://www.apache.org/licenses/LICENSE-2.0
  10.  *
  11.  * Unless required by applicable law or agreed to in writing, software
  12.  * distributed under the License is distributed on an "AS IS" BASIS,
  13.  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14.  * See the License for the specific language governing permissions and
  15.  * limitations under the License.
  16.  */
  17. package org.orekit.files.ilrs;

  18. import java.io.BufferedReader;
  19. import java.io.IOException;
  20. import java.io.InputStream;
  21. import java.io.InputStreamReader;
  22. import java.nio.charset.StandardCharsets;
  23. import java.nio.file.Files;
  24. import java.nio.file.Paths;
  25. import java.util.Optional;
  26. import java.util.regex.Pattern;
  27. import java.util.stream.Stream;

  28. import org.hipparchus.geometry.euclidean.threed.Vector3D;
  29. import org.orekit.annotation.DefaultDataContext;
  30. import org.orekit.data.DataContext;
  31. import org.orekit.errors.OrekitException;
  32. import org.orekit.errors.OrekitMessages;
  33. import org.orekit.files.general.EphemerisFileParser;
  34. import org.orekit.files.ilrs.CPFFile.CPFCoordinate;
  35. import org.orekit.frames.Frame;
  36. import org.orekit.frames.Frames;
  37. import org.orekit.time.AbsoluteDate;
  38. import org.orekit.time.DateComponents;
  39. import org.orekit.time.TimeScale;
  40. import org.orekit.utils.CartesianDerivativesFilter;
  41. import org.orekit.utils.Constants;
  42. import org.orekit.utils.IERSConventions;

  43. /**
  44.  * A parser for the CPF orbit file format.
  45.  * <p>
  46.  * It supports both 1.0 and 2.0 versions
  47.  * <p>
  48.  * <b>Note:</b> Only required header keys are read. Furthermore, only position data are read.
  49.  * Other keys are simply ignored
  50.  * Contributions are welcome to support more fields in the format.
  51.  * </p>
  52.  * @see <a href="https://ilrs.gsfc.nasa.gov/docs/2006/cpf_1.01.pdf">1.0 file format</a>
  53.  * @see <a href="https://ilrs.gsfc.nasa.gov/docs/2018/cpf_2.00h-1.pdf">2.0 file format</a>
  54.  * @author Bryan Cazabonne
  55.  * @since 10.3
  56.  */
  57. public class CPFParser implements EphemerisFileParser {

  58.     /** File format. */
  59.     private static final String FILE_FORMAT = "CPF";

  60.     /** Miscroseconds to seconds converter. */
  61.     private static final double MS_TO_S = 1.0e-6;

  62.     /** Pattern for delimiting regular expressions. */
  63.     private static final Pattern SEPARATOR = Pattern.compile("\\s+");

  64.     /** Default number of sample for interpolating data (See: reference documents. */
  65.     private static final int DEFAULT_INTERPOLATION_SAMPLE = 10;

  66.     /** Standard gravitational parameter in m^3 / s^2. */
  67.     private final double mu;

  68.     /** Time scale used to define epochs in CPF file. */
  69.     private final TimeScale timeScale;

  70.     /** Set of frames. */
  71.     private final Frames frames;

  72.     /** Interpolation sample for data interpolating. */
  73.     private final int interpolationSample;

  74.     /** IERS convention for frames. */
  75.     private final IERSConventions iersConvention;

  76.     /**
  77.      * Default constructor.
  78.      * <p>
  79.      * This constructor uses the {@link DataContext#getDefault() default data context}.
  80.      */
  81.     @DefaultDataContext
  82.     public CPFParser() {
  83.         this(Constants.EIGEN5C_EARTH_MU, DEFAULT_INTERPOLATION_SAMPLE,
  84.              IERSConventions.IERS_2010, DataContext.getDefault().getTimeScales().getUTC(),
  85.              DataContext.getDefault().getFrames());
  86.     }

  87.     /**
  88.      * Constructor.
  89.      * @param mu standard gravitational parameter to use for
  90.      *           creating {@link org.orekit.orbits.Orbit Orbits} from
  91.      *           the ephemeris data.
  92.      * @param interpolationSamples number of samples to use when interpolating
  93.      * @param iersConventions IERS convention for frames definition
  94.      * @param utc time scale used to define epochs in CPF files (UTC)
  95.      * @param frames set of frames for satellite coordinates
  96.      */
  97.     public CPFParser(final double mu,
  98.                      final int interpolationSamples,
  99.                      final IERSConventions iersConventions,
  100.                      final TimeScale utc,
  101.                      final Frames frames) {
  102.         this.mu                  = mu;
  103.         this.interpolationSample = interpolationSamples;
  104.         this.iersConvention      = iersConventions;
  105.         this.timeScale           = utc;
  106.         this.frames              = frames;
  107.     }

  108.     /**
  109.      * Parse a CPF file from an input stream using the UTF-8 charset.
  110.      *
  111.      * <p> This method creates a {@link BufferedReader} from the stream and as such this
  112.      * method may read more data than necessary from {@code stream} and the additional
  113.      * data will be lost. The other parse methods do not have this issue.
  114.      *
  115.      * @param stream to read the CPF file from.
  116.      * @return a parsed CPF file.
  117.      * @throws IOException if {@code stream} throws one.
  118.      * @see #parse(String)
  119.      * @see #parse(BufferedReader, String)
  120.      */
  121.     public CPFFile parse(final InputStream stream) throws IOException {
  122.         try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8))) {
  123.             return parse(reader, stream.toString());
  124.         }
  125.     }

  126.     /** {@inheritDoc} */
  127.     @Override
  128.     public CPFFile parse(final String fileName) throws IOException {
  129.         try (BufferedReader reader = Files.newBufferedReader(Paths.get(fileName),
  130.                                                              StandardCharsets.UTF_8)) {
  131.             return parse(reader, fileName);
  132.         }
  133.     }

  134.     /** {@inheritDoc} */
  135.     @Override
  136.     public CPFFile parse(final BufferedReader reader,
  137.                          final String fileName) throws IOException {

  138.         // initialize internal data structures
  139.         final ParseInfo pi = new ParseInfo();

  140.         int lineNumber = 0;
  141.         Stream<LineParser> parsers = Stream.of(LineParser.H1);
  142.         for (String line = reader.readLine(); line != null; line = reader.readLine()) {
  143.             ++lineNumber;
  144.             final String l = line;
  145.             final Optional<LineParser> selected = parsers.filter(p -> p.canHandle(l)).findFirst();
  146.             if (selected.isPresent()) {
  147.                 try {
  148.                     selected.get().parse(line, pi);
  149.                 } catch (StringIndexOutOfBoundsException | NumberFormatException e) {
  150.                     throw new OrekitException(e,
  151.                                               OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
  152.                                               lineNumber, fileName, line);
  153.                 }
  154.                 parsers = selected.get().allowedNext();
  155.             }
  156.             if (pi.done) {
  157.                 pi.file.setFilter(pi.hasVelocityEntries ?
  158.                                   CartesianDerivativesFilter.USE_PV :
  159.                                   CartesianDerivativesFilter.USE_P);
  160.                 // Return file
  161.                 return pi.file;
  162.             }
  163.         }

  164.         // We never reached the EOF marker
  165.         throw new OrekitException(OrekitMessages.CPF_UNEXPECTED_END_OF_FILE, lineNumber);

  166.     }

  167.     /** Transient data used for parsing a CPF file. The data is kept in a
  168.      * separate data structure to make the parser thread-safe.
  169.      * <p><b>Note</b>: The class intentionally does not provide accessor
  170.      * methods, as it is only used internally for parsing a CPF file.</p>
  171.      */
  172.     private class ParseInfo {

  173.         /** The corresponding CPF file. */
  174.         private CPFFile file;

  175.         /** IERS convention. */
  176.         private IERSConventions convention;

  177.         /** Set of frames. */
  178.         private Frames frames;

  179.         /** Frame for the ephemeris data. */
  180.         private Frame frame;

  181.         /** Time scale. */
  182.         private TimeScale timeScale;

  183.         /** Indicates if the SP3 file has velocity entries. */
  184.         private boolean hasVelocityEntries;

  185.         /** End Of File reached indicator. */
  186.         private boolean done;

  187.         /**
  188.          * Constructor.
  189.          */
  190.         protected ParseInfo() {

  191.             // Initialise file
  192.             file = new CPFFile();

  193.             // Time scale
  194.             this.timeScale = CPFParser.this.timeScale;

  195.             // Initialise fields
  196.             file.setMu(mu);
  197.             file.setInterpolationSample(interpolationSample);
  198.             file.setTimeScale(timeScale);

  199.             // Default values
  200.             this.done               = false;
  201.             this.hasVelocityEntries = false;

  202.             // Default value for reference frame
  203.             this.convention = CPFParser.this.iersConvention;
  204.             this.frames     = CPFParser.this.frames;
  205.             frame           = frames.getITRF(convention, false);

  206.         }

  207.     }

  208.     /** Parsers for specific lines. */
  209.     private enum LineParser {

  210.         /** Header first line. */
  211.         H1("H1") {

  212.             /** {@inheritDoc} */
  213.             @Override
  214.             public void parse(final String line, final ParseInfo pi) {

  215.                 // Data contained in the line
  216.                 final String[] values = SEPARATOR.split(line);

  217.                 // Index for reading data.
  218.                 // Allow taking into consideration difference between 1.0 and 2.0 formats
  219.                 int index = 1;

  220.                 // Format
  221.                 final String format = values[index++];

  222.                 // Throw an exception if format is not equal to "CPF"
  223.                 if (!FILE_FORMAT.equals(format)) {
  224.                     throw new OrekitException(OrekitMessages.UNEXPECTED_FORMAT_FOR_ILRS_FILE, FILE_FORMAT, format);
  225.                 }

  226.                 // Fill first elements
  227.                 pi.file.getHeader().setFormat(format);
  228.                 pi.file.getHeader().setVersion(Integer.parseInt(values[index++]));
  229.                 pi.file.getHeader().setSource(values[index++]);

  230.                 // Epoch of ephemeris production
  231.                 final int year  = Integer.parseInt(values[index++]);
  232.                 final int month = Integer.parseInt(values[index++]);
  233.                 final int day   = Integer.parseInt(values[index++]);
  234.                 pi.file.getHeader().setProductionEpoch(new DateComponents(year, month, day));

  235.                 // Hour of ephemeris production
  236.                 pi.file.getHeader().setProductionHour(Integer.parseInt(values[index++]));

  237.                 // Ephemeris sequence number
  238.                 pi.file.getHeader().setSequenceNumber(Integer.parseInt(values[index++]));

  239.                 // Difference between version 1.0 and 2.0: sub-daily ephemeris sequence number
  240.                 if (pi.file.getHeader().getVersion() == 2) {
  241.                     pi.file.getHeader().setSubDailySequenceNumber(Integer.parseInt(values[index++]));
  242.                 }

  243.                 // Target Name
  244.                 pi.file.getHeader().setName(values[index]);

  245.             }

  246.             /** {@inheritDoc} */
  247.             @Override
  248.             public Stream<LineParser> allowedNext() {
  249.                 return Stream.of(H2, ZERO);
  250.             }

  251.         },

  252.         /** Header second line. */
  253.         H2("H2") {

  254.             /** {@inheritDoc} */
  255.             @Override
  256.             public void parse(final String line, final ParseInfo pi) {

  257.                 // Data contained in the line
  258.                 final String[] values = SEPARATOR.split(line);

  259.                 // Identifiers
  260.                 pi.file.getHeader().setIlrsSatelliteId(values[1]);
  261.                 pi.file.getHeader().setSic(values[2]);
  262.                 pi.file.getHeader().setNoradId(values[3]);

  263.                 // Start epoch
  264.                 final int    yearS   = Integer.parseInt(values[4]);
  265.                 final int    monthS  = Integer.parseInt(values[5]);
  266.                 final int    dayS    = Integer.parseInt(values[6]);
  267.                 final int    hourS   = Integer.parseInt(values[7]);
  268.                 final int    minuteS = Integer.parseInt(values[8]);
  269.                 final double secondS = Integer.parseInt(values[9]);

  270.                 pi.file.getHeader().setStartEpoch(new AbsoluteDate(yearS, monthS, dayS,
  271.                                                                    hourS, minuteS, secondS,
  272.                                                                    pi.file.getTimeScale()));

  273.                 // End epoch
  274.                 final int    yearE   = Integer.parseInt(values[10]);
  275.                 final int    monthE  = Integer.parseInt(values[11]);
  276.                 final int    dayE    = Integer.parseInt(values[12]);
  277.                 final int    hourE   = Integer.parseInt(values[13]);
  278.                 final int    minuteE = Integer.parseInt(values[14]);
  279.                 final double secondE = Integer.parseInt(values[15]);

  280.                 pi.file.getHeader().setEndEpoch(new AbsoluteDate(yearE, monthE, dayE,
  281.                                                                  hourE, minuteE, secondE,
  282.                                                                  pi.file.getTimeScale()));

  283.                 // Time between table entries
  284.                 pi.file.getHeader().setStep(Integer.parseInt(values[16]));

  285.                 // Compatibility with TIVs
  286.                 pi.file.getHeader().setIsCompatibleWithTIVs(Integer.parseInt(values[17]) == 1);

  287.                 // Target class
  288.                 pi.file.getHeader().setTargetClass(Integer.parseInt(values[18]));

  289.                 // Reference frame
  290.                 final int frameId = Integer.parseInt(values[19]);
  291.                 switch (frameId) {
  292.                     case 0:
  293.                         pi.frame = pi.frames.getITRF(pi.convention, false);
  294.                         break;
  295.                     case 1:
  296.                         pi.frame = pi.frames.getTOD(true);
  297.                         break;
  298.                     case 2:
  299.                         pi.frame = pi.frames.getMOD(pi.convention);
  300.                         break;
  301.                     default:
  302.                         pi.frame = pi.frames.getITRF(pi.convention, false);
  303.                         break;
  304.                 }
  305.                 pi.file.getHeader().setRefFrame(pi.frame);
  306.                 pi.file.getHeader().setRefFrameId(frameId);

  307.                 // Last fields
  308.                 pi.file.getHeader().setRotationalAngleType(Integer.parseInt(values[20]));
  309.                 pi.file.getHeader().setIsCenterOfMassCorrectionApplied(Integer.parseInt(values[21]) == 1);
  310.                 if (pi.file.getHeader().getVersion() == 2) {
  311.                     pi.file.getHeader().setTargetLocation(Integer.parseInt(values[22]));
  312.                 }

  313.             }

  314.             /** {@inheritDoc} */
  315.             @Override
  316.             public Stream<LineParser> allowedNext() {
  317.                 return Stream.of(H3, H4, H5, H9, ZERO);
  318.             }

  319.         },

  320.         /** Header third line. */
  321.         H3("H3") {

  322.             /** {@inheritDoc} */
  323.             @Override
  324.             public void parse(final String line, final ParseInfo pi) {
  325.                 // Not implemented yet
  326.             }

  327.             /** {@inheritDoc} */
  328.             @Override
  329.             public Stream<LineParser> allowedNext() {
  330.                 return Stream.of(H4, H5, H9, ZERO);
  331.             }

  332.         },

  333.         /** Header fourth line. */
  334.         H4("H4") {

  335.             /** {@inheritDoc} */
  336.             @Override
  337.             public void parse(final String line, final ParseInfo pi) {

  338.                 // Data contained in the line
  339.                 final String[] values = SEPARATOR.split(line);

  340.                 // Pulse Repetition Frequency (PRF)
  341.                 pi.file.getHeader().setPrf(Double.parseDouble(values[1]));

  342.                 // Transponder information
  343.                 pi.file.getHeader().setTranspTransmitDelay(Double.parseDouble(values[2]) * MS_TO_S);
  344.                 pi.file.getHeader().setTranspUtcOffset(Double.parseDouble(values[3]) * MS_TO_S);
  345.                 pi.file.getHeader().setTranspOscDrift(Double.parseDouble(values[4]));
  346.                 if (pi.file.getHeader().getVersion() == 2) {
  347.                     pi.file.getHeader().setTranspClkRef(Double.parseDouble(values[5]));
  348.                 }

  349.             }

  350.             /** {@inheritDoc} */
  351.             @Override
  352.             public Stream<LineParser> allowedNext() {
  353.                 return Stream.of(H5, H9, ZERO);
  354.             }

  355.         },

  356.         /** Header fifth line. */
  357.         H5("H5") {

  358.             /** {@inheritDoc} */
  359.             @Override
  360.             public void parse(final String line, final ParseInfo pi) {

  361.                 // Approximate center of mass to reflector offset in meters
  362.                 final double offset = Double.parseDouble(SEPARATOR.split(line)[1]);
  363.                 pi.file.getHeader().setCenterOfMassOffset(offset);

  364.             }

  365.             /** {@inheritDoc} */
  366.             @Override
  367.             public Stream<LineParser> allowedNext() {
  368.                 return Stream.of(H9, ZERO);
  369.             }

  370.         },

  371.         /** Header last line. */
  372.         H9("H9") {

  373.             /** {@inheritDoc} */
  374.             @Override
  375.             public void parse(final String line, final ParseInfo pi) {
  376.                 // End of header. Nothing to do
  377.             }

  378.             /** {@inheritDoc} */
  379.             @Override
  380.             public Stream<LineParser> allowedNext() {
  381.                 return Stream.of(TEN, ZERO);
  382.             }

  383.         },

  384.         /** Position values. */
  385.         TEN("10") {

  386.             /** {@inheritDoc} */
  387.             @Override
  388.             public void parse(final String line, final ParseInfo pi) {

  389.                 // Data contained in the line
  390.                 final String[] values = SEPARATOR.split(line);

  391.                 // Epoch
  392.                 final int mjd           = Integer.parseInt(values[2]);
  393.                 final double secInDay   = Double.parseDouble(values[3]);
  394.                 final AbsoluteDate date = AbsoluteDate.createMJDDate(mjd, secInDay, pi.timeScale);

  395.                 // Leap second flag
  396.                 final int leap = Integer.parseInt(values[4]);

  397.                 // Coordinates
  398.                 final double x = Double.parseDouble(values[5]);
  399.                 final double y = Double.parseDouble(values[6]);
  400.                 final double z = Double.parseDouble(values[7]);
  401.                 final Vector3D position = new Vector3D(x, y, z);

  402.                 // CPF coordinate
  403.                 final CPFCoordinate coordinate =
  404.                                 new CPFCoordinate(date, position, leap);
  405.                 pi.file.addSatelliteCoordinate(coordinate);

  406.             }

  407.             /** {@inheritDoc} */
  408.             @Override
  409.             public Stream<LineParser> allowedNext() {
  410.                 return Stream.of(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  411.             }

  412.         },

  413.         /** Velocity values. */
  414.         TWENTY("20") {

  415.             /** {@inheritDoc} */
  416.             @Override
  417.             public void parse(final String line, final ParseInfo pi) {
  418.                 // Not implemented yet
  419.             }

  420.             /** {@inheritDoc} */
  421.             @Override
  422.             public Stream<LineParser> allowedNext() {
  423.                 return Stream.of(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  424.             }

  425.         },

  426.         /** Corrections. */
  427.         THIRTY("30") {

  428.             /** {@inheritDoc} */
  429.             @Override
  430.             public void parse(final String line, final ParseInfo pi) {
  431.                 // Not implemented yet
  432.             }

  433.             /** {@inheritDoc} */
  434.             @Override
  435.             public Stream<LineParser> allowedNext() {
  436.                 return Stream.of(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  437.             }

  438.         },

  439.         /** Transponder specific. */
  440.         FORTY("40") {

  441.             /** {@inheritDoc} */
  442.             @Override
  443.             public void parse(final String line, final ParseInfo pi) {
  444.                 // Not implemented yet
  445.             }

  446.             /** {@inheritDoc} */
  447.             @Override
  448.             public Stream<LineParser> allowedNext() {
  449.                 return Stream.of(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  450.             }

  451.         },

  452.         /** Offset from center of main body. */
  453.         FIFTY("50") {

  454.             /** {@inheritDoc} */
  455.             @Override
  456.             public void parse(final String line, final ParseInfo pi) {
  457.                 // Not implemented yet
  458.             }

  459.             /** {@inheritDoc} */
  460.             @Override
  461.             public Stream<LineParser> allowedNext() {
  462.                 return Stream.of(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  463.             }

  464.         },

  465.         /** Rotation angle of offset. */
  466.         SIXTY("60") {

  467.             /** {@inheritDoc} */
  468.             @Override
  469.             public void parse(final String line, final ParseInfo pi) {
  470.                 // Not implemented yet
  471.             }

  472.             /** {@inheritDoc} */
  473.             @Override
  474.             public Stream<LineParser> allowedNext() {
  475.                 return Stream.of(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  476.             }

  477.         },

  478.         /** Earth orientation. */
  479.         SEVENTY("70") {

  480.             /** {@inheritDoc} */
  481.             @Override
  482.             public void parse(final String line, final ParseInfo pi) {
  483.                 // Not implemented yet
  484.             }

  485.             /** {@inheritDoc} */
  486.             @Override
  487.             public Stream<LineParser> allowedNext() {
  488.                 return Stream.of(TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  489.             }

  490.         },

  491.         /** Comments. */
  492.         ZERO("00") {

  493.             /** {@inheritDoc} */
  494.             @Override
  495.             public void parse(final String line, final ParseInfo pi) {

  496.                 // Comment
  497.                 final String comment = line.split(getIdentifier())[1].trim();
  498.                 pi.file.getComments().add(comment);

  499.             }

  500.             /** {@inheritDoc} */
  501.             @Override
  502.             public Stream<LineParser> allowedNext() {
  503.                 return Stream.of(H1, H2, H3, H4, H5, H9,
  504.                                  TEN, TWENTY, THIRTY, FORTY, FIFTY, SIXTY, SEVENTY, ZERO, EOF);
  505.             }

  506.         },

  507.         /** Last record in ephemeris. */
  508.         EOF("99") {

  509.             @Override
  510.             public void parse(final String line, final ParseInfo pi) {
  511.                 pi.done = true;
  512.             }

  513.             /** {@inheritDoc} */
  514.             @Override
  515.             public Stream<LineParser> allowedNext() {
  516.                 return Stream.of(EOF);
  517.             }

  518.         };

  519.         /** Pattern for identifying line. */
  520.         private final Pattern pattern;

  521.         /** Identifier. */
  522.         private final String identifier;

  523.         /** Simple constructor.
  524.          * @param identifier regular expression for identifying line (i.e. first element)
  525.          */
  526.         LineParser(final String identifier) {
  527.             this.identifier = identifier;
  528.             pattern = Pattern.compile(identifier);
  529.         }

  530.         /**
  531.          * Get the regular expression for identifying line.
  532.          * @return the regular expression for identifying line
  533.          */
  534.         public String getIdentifier() {
  535.             return identifier;
  536.         }

  537.         /** Parse a line.
  538.          * @param line line to parse
  539.          * @param pi holder for transient data
  540.          */
  541.         public abstract void parse(String line, ParseInfo pi);

  542.         /** Get the allowed parsers for next line.
  543.          * @return allowed parsers for next line
  544.          */
  545.         public abstract Stream<LineParser> allowedNext();

  546.         /** Check if parser can handle line.
  547.          * @param line line to parse
  548.          * @return true if parser can handle the specified line
  549.          */
  550.         public boolean canHandle(final String line) {
  551.             return pattern.matcher(SEPARATOR.split(line)[0]).matches();
  552.         }

  553.     }

  554. }