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 }