1   /* Copyright 2002-2025 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.time;
18  
19  import java.io.BufferedReader;
20  import java.io.IOException;
21  import java.io.InputStream;
22  import java.io.InputStreamReader;
23  import java.nio.charset.StandardCharsets;
24  import java.util.ArrayList;
25  import java.util.List;
26  import java.util.regex.Matcher;
27  import java.util.regex.Pattern;
28  
29  import org.orekit.annotation.DefaultDataContext;
30  import org.orekit.data.AbstractSelfFeedingLoader;
31  import org.orekit.data.DataContext;
32  import org.orekit.data.DataProvidersManager;
33  import org.orekit.errors.OrekitException;
34  import org.orekit.errors.OrekitMessages;
35  
36  
37  /** Loader for UTC versus TAI history files.
38   * <p>UTC versus TAI history files contain {@link UTCTAIOffset
39   * leap seconds} data since.</p>
40   * <p>The UTC versus TAI history files are recognized thanks to their
41   * base names, which must match the pattern <code>UTC-TAI.history</code>
42   * (or <code>UTC-TAI.history.gz</code> for gzip-compressed files)</p>
43   * <p>Only one history file must be present in the IERS directories
44   * hierarchy.</p>
45   * @author Luc Maisonobe
46   */
47  public class UTCTAIHistoryFilesLoader extends AbstractSelfFeedingLoader
48          implements UTCTAIOffsetsLoader {
49  
50      /** Supported files name pattern. */
51      private static final String SUPPORTED_NAMES = "^UTC-TAI\\.history$";
52  
53      /**
54       * Build a loader for UTC-TAI history file. This constructor uses the {@link
55       * DataContext#getDefault() default data context}.
56       *
57       * @see #UTCTAIHistoryFilesLoader(DataProvidersManager)
58       */
59      @DefaultDataContext
60      public UTCTAIHistoryFilesLoader() {
61          this(DataContext.getDefault().getDataProvidersManager());
62      }
63  
64      /**
65       * Build a loader for UTC-TAI history file.
66       *
67       * @param manager provides access to the {@code UTC-TAI.history} file.
68       */
69      public UTCTAIHistoryFilesLoader(final DataProvidersManager manager) {
70          super(SUPPORTED_NAMES, manager);
71      }
72  
73      /** {@inheritDoc} */
74      @Override
75      public List<OffsetModel> loadOffsets() {
76          final UtcTaiOffsetLoader parser = new UtcTaiOffsetLoader(new Parser());
77          this.feed(parser);
78          return parser.getOffsets();
79      }
80  
81      /** Internal class performing the parsing. */
82      public static class Parser implements UTCTAIOffsetsLoader.Parser {
83  
84          /** Regular data lines pattern. */
85          private final Pattern regularPattern;
86  
87          /** Last line pattern pattern. */
88          private final Pattern lastPattern;
89  
90          /** Simple constructor.
91           */
92          public Parser() {
93  
94              // the data lines in the UTC time steps data files have the following form:
95              // 1966  Jan.  1 - 1968  Feb.  1     4.313 170 0s + (MJD - 39 126) x 0.002 592s
96              // 1968  Feb.  1 - 1972  Jan.  1     4.213 170 0s +        ""
97              // 1972  Jan.  1 -       Jul.  1    10s
98              //       Jul.  1 - 1973  Jan.  1    11s
99              // 1973  Jan.  1 - 1974  Jan.  1    12s
100             //  ...
101             // 2006  Jan.  1.- 2009  Jan.  1    33s
102             // 2009  Jan.  1.- 2012  Jul   1    34s
103             // 2012  Jul   1 -                  35s
104 
105             // we ignore the non-constant and non integer offsets before 1972-01-01
106             final String start = "^";
107 
108             // year group
109             final String yearField = "\\p{Blank}*((?:\\p{Digit}\\p{Digit}\\p{Digit}\\p{Digit})|(?:    ))";
110 
111             // second group: month as a three letters capitalized abbreviation
112             final StringBuilder builder = new StringBuilder("\\p{Blank}+(");
113             for (final Month month : Month.values()) {
114                 builder.append(month.getCapitalizedAbbreviation());
115                 builder.append('|');
116             }
117             builder.delete(builder.length() - 1, builder.length());
118             builder.append(")\\.?");
119             final String monthField = builder.toString();
120 
121             // day group
122             final String dayField = "\\p{Blank}+([ 0-9]+)\\.?";
123 
124             // offset group
125             final String offsetField = "\\p{Blank}+(\\p{Digit}+)s";
126 
127             final String separator   = "\\p{Blank}*-\\p{Blank}+";
128             final String finalBlanks = "\\p{Blank}*$";
129             regularPattern = Pattern.compile(start + yearField + monthField + dayField +
130                                              separator + yearField + monthField + dayField +
131                                              offsetField + finalBlanks);
132             lastPattern    = Pattern.compile(start + yearField + monthField + dayField +
133                                              separator + offsetField + finalBlanks);
134 
135 
136         }
137 
138         /** Load UTC-TAI offsets entries read from some file.
139          *
140          * {@inheritDoc}
141          *
142          * @param input data input stream
143          * @param name name of the file (or zip entry)
144          * @exception IOException if data can't be read
145          */
146         @Override
147         public List<OffsetModel> parse(final InputStream input, final String name)
148             throws IOException {
149 
150             final List<OffsetModel> offsets = new ArrayList<>();
151             final String emptyYear = "    ";
152             int lineNumber = 0;
153             DateComponents lastDate = null;
154             String line = null;
155             int lastLine = 0;
156             String previousYear = emptyYear;
157             // set up a reader for line-oriented file
158             try (BufferedReader reader = new BufferedReader(new InputStreamReader(input, StandardCharsets.UTF_8))) {
159 
160                 // read all file, ignoring not recognized lines
161                 for (line = reader.readLine(); line != null; line = reader.readLine()) {
162                     ++lineNumber;
163 
164                     // check matching for regular lines and last line
165                     Matcher matcher = regularPattern.matcher(line);
166                     if (matcher.matches()) {
167                         if (lastLine > 0) {
168                             throw new OrekitException(OrekitMessages.UNEXPECTED_DATA_AFTER_LINE_IN_FILE,
169                                                       lastLine, name, line);
170                         }
171                     } else {
172                         matcher = lastPattern.matcher(line);
173                         if (matcher.matches()) {
174                             // this is the last line (there is a start date but no end date)
175                             lastLine = lineNumber;
176                         }
177                     }
178 
179                     if (matcher.matches()) {
180 
181                         // build an entry from the extracted fields
182 
183                         String year = matcher.group(1);
184                         if (emptyYear.equals(year)) {
185                             year = previousYear;
186                         }
187                         if (lineNumber != lastLine) {
188                             if (emptyYear.equals(matcher.group(4))) {
189                                 previousYear = year;
190                             } else {
191                                 previousYear = matcher.group(4);
192                             }
193                         }
194                         final DateComponents leapDay = new DateComponents(Integer.parseInt(year.trim()),
195                                                                           Month.parseMonth(matcher.group(2)),
196                                                                           Integer.parseInt(matcher.group(3).trim()));
197 
198                         final int offset = Integer.parseInt(matcher.group(matcher.groupCount()));
199                         if (lastDate != null && leapDay.compareTo(lastDate) <= 0) {
200                             throw new OrekitException(OrekitMessages.NON_CHRONOLOGICAL_DATES_IN_FILE,
201                                                       name, lineNumber);
202                         }
203                         lastDate = leapDay;
204                         offsets.add(new OffsetModel(leapDay, offset));
205 
206                     }
207                 }
208 
209             }  catch (NumberFormatException nfe) {
210                 throw new OrekitException(OrekitMessages.UNABLE_TO_PARSE_LINE_IN_FILE,
211                                           lineNumber, name, line);
212             }
213 
214             if (offsets.isEmpty()) {
215                 throw new OrekitException(OrekitMessages.NO_ENTRIES_IN_IERS_UTC_TAI_HISTORY_FILE, name);
216             }
217 
218             return offsets;
219         }
220 
221     }
222 
223 }