• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

oracle / opengrok / #3691

09 Nov 2023 04:15PM UTC coverage: 74.721% (+8.6%) from 66.08%
#3691

push

web-flow
avoid annotations for binary files (#4476)

fixes #4473

6 of 13 new or added lines in 4 files covered. (46.15%)

258 existing lines in 28 files now uncovered.

43797 of 58614 relevant lines covered (74.72%)

0.75 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

64.52
/opengrok-indexer/src/main/java/org/opengrok/indexer/history/Repository.java
1
/*
2
 * CDDL HEADER START
3
 *
4
 * The contents of this file are subject to the terms of the
5
 * Common Development and Distribution License (the "License").
6
 * You may not use this file except in compliance with the License.
7
 *
8
 * See LICENSE.txt included in this distribution for the specific
9
 * language governing permissions and limitations under the License.
10
 *
11
 * When distributing Covered Code, include this CDDL HEADER in each
12
 * file and include the License file at LICENSE.txt.
13
 * If applicable, add the following below this CDDL HEADER, with the
14
 * fields enclosed by brackets "[]" replaced with your own identifying
15
 * information: Portions Copyright [yyyy] [name of copyright owner]
16
 *
17
 * CDDL HEADER END
18
 */
19

20
/*
21
 * Copyright (c) 2008, 2023, Oracle and/or its affiliates. All rights reserved.
22
 * Portions Copyright (c) 2017, 2020, Chris Fraire <cfraire@me.com>.
23
 */
24
package org.opengrok.indexer.history;
25

26
import java.io.ByteArrayInputStream;
27
import java.io.ByteArrayOutputStream;
28
import java.io.File;
29
import java.io.FileOutputStream;
30
import java.io.IOException;
31
import java.io.InputStream;
32
import java.io.OutputStream;
33
import java.text.DateFormat;
34
import java.text.FieldPosition;
35
import java.text.ParseException;
36
import java.text.ParsePosition;
37
import java.text.SimpleDateFormat;
38
import java.util.ArrayList;
39
import java.util.Date;
40
import java.util.Iterator;
41
import java.util.List;
42
import java.util.Locale;
43
import java.util.TreeSet;
44
import java.util.logging.Level;
45
import java.util.logging.Logger;
46

47
import org.jetbrains.annotations.Nullable;
48
import org.opengrok.indexer.configuration.CommandTimeoutType;
49
import org.opengrok.indexer.configuration.RuntimeEnvironment;
50
import org.opengrok.indexer.logger.LoggerFactory;
51
import org.opengrok.indexer.util.BufferSink;
52
import org.opengrok.indexer.util.Executor;
53

54
/**
55
 * An interface for an external repository.
56
 *
57
 * @author Trond Norbye
58
 */
59
public abstract class Repository extends RepositoryInfo {
60

61
    private static final long serialVersionUID = -203179700904894217L;
62

63
    private static final Logger LOGGER = LoggerFactory.getLogger(Repository.class);
1✔
64

65
    /**
66
     * format used for printing the date in {@code currentVersion}.
67
     * <p>
68
     * NOTE: SimpleDateFormat is not thread-safe, lock must be held when formatting
69
     * </p>
70
     */
71
    protected static final SimpleDateFormat OUTPUT_DATE_FORMAT =
1✔
72
            new SimpleDateFormat("yyyy-MM-dd HH:mm Z");
73

74
    /**
75
     * The command with which to access the external repository. Can be
76
     * {@code null} if the repository isn't accessed via a CLI, or if it hasn't
77
     * been initialized by {@link #ensureCommand} yet.
78
     */
79
    protected String RepoCommand;
80

81
    protected final List<String> ignoredFiles;
82

83
    protected final List<String> ignoredDirs;
84

85
    /**
86
     * List of &lt;revision, tags&gt; pairs for repositories which display tags
87
     * only for files changed by the tagged commit.
88
     */
89
    protected TreeSet<TagEntry> tagList = null;
1✔
90

91
    abstract boolean fileHasHistory(File file);
92

93
    /**
94
     * Check if the repository supports {@code getHistory()} requests for whole
95
     * directories at once.
96
     *
97
     * @return {@code true} if the repository can get history for directories
98
     */
99
    abstract boolean hasHistoryForDirectories();
100

101
    /**
102
     * Get the history for the specified file or directory.
103
     * It is expected that {@link History#getRenamedFiles()} and {@link HistoryEntry#getFiles()} are empty for files.
104
     *
105
     * @param file the file to get the history for
106
     * @return history log for file
107
     * @throws HistoryException on error accessing the history
108
     */
109
    abstract History getHistory(File file) throws HistoryException;
110

111
    /**
112
     * This is generic implementation that retrieves the full history of given file
113
     * and returns the latest history entry. This is obviously very inefficient, both in terms of memory and I/O.
114
     * The extending classes are encouraged to implement their own version.
115
     * @param file file
116
     * @return last history entry or null
117
     * @throws HistoryException on error
118
     */
119
    public HistoryEntry getLastHistoryEntry(File file, boolean ui) throws HistoryException {
120
        History history;
121
        try {
122
            history = HistoryGuru.getInstance().getHistory(file, false, ui);
×
123
        } catch (HistoryException ex) {
1✔
124
            LOGGER.log(Level.WARNING, "failed to get history for {0}", file);
1✔
125
            return null;
1✔
126
        }
×
127

128
        if (history != null) {
×
129
            return history.getLastHistoryEntry();
×
130
        } else {
131
            return null;
×
132
        }
133
    }
134

135
    protected Repository() {
136
        super();
1✔
137
        ignoredFiles = new ArrayList<>();
1✔
138
        ignoredDirs = new ArrayList<>();
1✔
139
    }
1✔
140

141
    /**
142
     * Gets the instance's repository command, primarily for testing purposes.
143
     * @return null if not {@link #isWorking()}, or otherwise a defined command
144
     */
145
    public String getRepoCommand() {
146
        isWorking();
1✔
147
        return RepoCommand;
1✔
148
    }
149

150
    /**
151
     * <p>
152
     * Get the history after a specified revision.
153
     * <p>
154
     * <p>The default implementation first fetches the full history and then throws
155
     * away the oldest revisions. This is not efficient, so subclasses should
156
     * override it in order to get good performance. Once every subclass has
157
     * implemented a more efficient method, the default implementation should be
158
     * removed and made abstract.
159
     *
160
     * @param file the file to get the history for
161
     * @param sinceRevision the revision right before the first one to return,
162
     * or {@code null} to return the full history
163
     * @return partial history for file
164
     * @throws HistoryException on error accessing the history
165
     */
166
    History getHistory(File file, String sinceRevision) throws HistoryException {
167

168
        // If we want an incremental history update and get here, warn that
169
        // it may be slow.
170
        if (sinceRevision != null) {
×
171
            LOGGER.log(Level.WARNING,
×
172
                    "Incremental history retrieval is not implemented for {0}.",
173
                    getClass().getSimpleName());
×
174
            LOGGER.log(Level.WARNING, "Falling back to slower full history retrieval.");
×
175
        }
176

177
        History history = getHistory(file);
×
178

179
        if (sinceRevision == null) {
×
180
            return history;
×
181
        }
182

183
        List<HistoryEntry> partial = new ArrayList<>();
×
184
        for (HistoryEntry entry : history.getHistoryEntries()) {
×
185
            partial.add(entry);
×
186
            if (sinceRevision.equals(entry.getRevision())) {
×
187
                // Found revision right before the first one to return.
188
                break;
×
189
            }
190
        }
×
191

192
        removeAndVerifyOldestChangeset(partial, sinceRevision);
×
193
        history.setHistoryEntries(partial);
×
194
        return history;
×
195
    }
196

197
    /**
198
     * Remove the oldest changeset from a list (assuming sorted with most recent
199
     * changeset first) and verify that it is the changeset we expected to find
200
     * there.
201
     *
202
     * @param entries a list of {@code HistoryEntry} objects
203
     * @param revision the revision we expect the oldest entry to have
204
     * @throws HistoryException if the oldest entry was not the one we expected
205
     */
206
    void removeAndVerifyOldestChangeset(List<HistoryEntry> entries, String revision) throws HistoryException {
207

208
        HistoryEntry entry = entries.isEmpty() ? null : entries.remove(entries.size() - 1);
×
209

210
        // TODO We should check more thoroughly that the changeset is the one
211
        // we expected it to be, since some SCMs may change the revision
212
        // numbers so that identical revision numbers does not always mean
213
        // identical changesets. We could for example get the cached changeset
214
        // and compare more fields, like author and date.
215
        if (entry == null || !revision.equals(entry.getRevision())) {
×
216
            throw new HistoryException("Cached revision '" + revision
×
217
                    + "' not found in the repository "
218
                    + getDirectoryName());
×
219
        }
220
    }
×
221

222
    /**
223
     * Gets the contents of a specific version of a named file, and copies
224
     * into the specified target file.
225
     *
226
     * @param target a required target file which will be overwritten
227
     * @param parent the name of the directory containing the file
228
     * @param basename the name of the file to get
229
     * @param rev the revision to get
230
     * @return {@code true} if contents were found
231
     * @throws java.io.IOException if an I/O error occurs
232
     */
233
    public boolean getHistoryGet(File target, String parent, String basename, String rev) throws IOException {
234
        try (FileOutputStream out = new FileOutputStream(target)) {
×
235
            return getHistoryGet(out, parent, basename, rev);
×
236
        }
237
    }
238

239
    /**
240
     * Gets an {@link InputStream} of the contents of a specific version of a
241
     * named file.
242
     * @param parent the name of the directory containing the file
243
     * @param basename the name of the file to get
244
     * @param rev the revision to get
245
     * @return a defined instance if contents were found; or else {@code null}
246
     */
247
    @Nullable
248
    public InputStream getHistoryGet(String parent, String basename, String rev) {
249
        ByteArrayOutputStream out = new ByteArrayOutputStream();
1✔
250
        if (getHistoryGet(out, parent, basename, rev)) {
1✔
251
            return new ByteArrayInputStream(out.toByteArray());
1✔
252
        }
253
        return null;
1✔
254
    }
255

256
    /**
257
     * Subclasses must override to get the contents of a specific version of a
258
     * named file, and copy to the specified {@code sink}.
259
     *
260
     * @param out a defined instance of OutputStream
261
     * @param parent the name of the directory containing the file
262
     * @param basename the name of the file to get
263
     * @param rev the revision to get
264
     * @return a value indicating if the get was successful.
265
     */
266
    abstract boolean getHistoryGet(OutputStream out, String parent, String basename, String rev);
267

268
    /**
269
     * Checks whether this parser can annotate files.
270
     *
271
     * @param file file to check
272
     * @return <code>true</code> if annotation is supported
273
     */
274
    abstract boolean fileHasAnnotation(File file);
275

276
    /**
277
     * Returns if this repository tags only files changed in last commit, i.e.
278
     * if we need to prepare list of repository-wide tags prior to creation of file history entries.
279
     *
280
     * @return True if we need tag list creation prior to file parsing, false by default.
281
     */
282
    boolean hasFileBasedTags() {
283
        return false;
×
284
    }
285

286
    TreeSet<TagEntry> getTagList() {
287
        return this.tagList;
1✔
288
    }
289

290
    /**
291
     * Assign tags to changesets they represent. The complete list of tags must
292
     * be pre-built using {@code getTagList()}. Then this function squeezes all
293
     * tags to changesets which actually exist in the history of given file.
294
     * Must be implemented repository-specific.
295
     *
296
     * @see #getTagList
297
     * @param hist History object we want to assign tags to.
298
     */
299
    void assignTagsInHistory(History hist) {
300
        if (hist == null) {
1✔
301
            return;
×
302
        }
303

304
        if (this.getTagList() == null) {
1✔
305
            if (RuntimeEnvironment.getInstance().isIndexer()) {
1✔
306
                throw new IllegalStateException("getTagList() is null");
1✔
307
            } else {
308
                return;
1✔
309
            }
310
        }
311

UNCOV
312
        Iterator<TagEntry> it = this.getTagList().descendingIterator();
×
UNCOV
313
        TagEntry lastTagEntry = null;
×
UNCOV
314
        for (HistoryEntry ent : hist.getHistoryEntries()) {
×
315
            // Assign all tags created since the last revision
316
            // TODO: is there better way to do this? We need to "repeat"
317
            //   last element returned by call to next()
UNCOV
318
            while (lastTagEntry != null || it.hasNext()) {
×
UNCOV
319
                if (lastTagEntry == null) {
×
UNCOV
320
                    lastTagEntry = it.next();
×
321
                }
UNCOV
322
                if (lastTagEntry.compareTo(ent) >= 0) {
×
UNCOV
323
                    hist.addTags(ent, lastTagEntry.getTags());
×
324
                } else {
325
                    break;
326
                }
UNCOV
327
                if (it.hasNext()) {
×
UNCOV
328
                    lastTagEntry = it.next();
×
329
                } else {
UNCOV
330
                    lastTagEntry = null;
×
331
                }
332
            }
UNCOV
333
        }
×
UNCOV
334
    }
×
335

336
    /**
337
     * Create internal list of all tags in this repository.
338
     *
339
     * @param directory directory of the repository
340
     * @param cmdType command timeout type
341
     */
342
    protected void buildTagList(File directory, CommandTimeoutType cmdType) {
343
        this.tagList = null;
×
344
    }
×
345

346
    /**
347
     * Annotate the specified revision of a file.
348
     *
349
     * @param file the file to annotate
350
     * @param revision revision of the file. Either {@code null} (for latest revision) or a non-empty string.
351
     * @return an <code>Annotation</code> object or {@code null}
352
     * @throws java.io.IOException if an error occurs
353
     */
354
    abstract @Nullable Annotation annotate(File file, @Nullable String revision) throws IOException;
355

356
    /**
357
     * Return revision for annotate view.
358
     *
359
     * @param historyRevision full revision
360
     * @return revision string suitable for matching into annotation
361
     */
362
    protected String getRevisionForAnnotate(String historyRevision) {
363
        return historyRevision;
1✔
364
    }
365

366
    protected void doCreateCache(HistoryCache cache, String sinceRevision, File directory)
367
            throws CacheException, HistoryException {
368
        History history = getHistory(directory, sinceRevision);
×
369
        finishCreateCache(cache, history, null);
×
370
    }
×
371

372
    /**
373
     * Create history cache for all files in this repository.
374
     * {@code getHistory()} is used to fetch the history for the entire
375
     * repository. If {@code hasHistoryForDirectories()} returns {@code false},
376
     * this method is a no-op.
377
     *
378
     * @param cache the cache instance in which to store the history log
379
     * @param sinceRevision if non-null, incrementally update the cache with all
380
     * revisions after the specified revision; otherwise, create the full
381
     * history starting with the initial revision
382
     *
383
     * @throws HistoryException on error
384
     */
385
    final void createCache(HistoryCache cache, String sinceRevision) throws HistoryException, CacheException {
386

387
        if (!isWorking()) {
1✔
388
            return;
×
389
        }
390

391
        // If it is not possible to get history for a directory, we can't create the cache
392
        // this way. Just give up and return.
393
        if (!hasHistoryForDirectories()) {
1✔
394
            LOGGER.log(Level.INFO,
1✔
395
                    "Skipping creation of history cache for {0}, since retrieval "
396
                            + "of history for directories is not implemented for this "
397
                            + "repository type.", getDirectoryName());
1✔
398
            return;
1✔
399
        }
400

401
        File directory = new File(getDirectoryName());
1✔
402

403
        doCreateCache(cache, sinceRevision, directory);
1✔
404

405
        LOGGER.log(Level.FINE, "Done storing history cache for repository {0}", getDirectoryName());
1✔
406
    }
1✔
407

408
    /**
409
     * Actually store the history in history cache.
410
     * @param cache history cache object
411
     * @param history history to store
412
     * @param tillRevision end revision (matters only for renamed files), can be null
413
     * @throws CacheException on error
414
     */
415
    void finishCreateCache(HistoryCache cache, History history, String tillRevision) throws CacheException {
416
        // We need to refresh list of tags for incremental reindex.
417
        RuntimeEnvironment env = RuntimeEnvironment.getInstance();
1✔
418
        if (env.isTagsEnabled() && this.hasFileBasedTags()) {
1✔
UNCOV
419
            this.buildTagList(new File(this.getDirectoryName()), CommandTimeoutType.INDEXER);
×
420
        }
421

422
        if (history != null) {
1✔
423
            cache.store(history, this, tillRevision);
1✔
424
        }
425
    }
1✔
426

427
    /**
428
     * Check if this it the right repository type for the given file.
429
     *
430
     * @param file File to check if this is a repository for.
431
     * @param cmdType command timeout type
432
     * @return true if this is the correct repository for this file/directory.
433
     */
434
    abstract boolean isRepositoryFor(File file, CommandTimeoutType cmdType);
435

436
    public final boolean isRepositoryFor(File file) {
437
        return isRepositoryFor(file, CommandTimeoutType.INDEXER);
×
438
    }
439

440
    /**
441
     * Determine parent of this repository.
442
     */
443
    abstract String determineParent(CommandTimeoutType cmdType) throws IOException;
444

445
    /**
446
     * Determine parent of this repository.
447
     * @return parent
448
     * @throws java.io.IOException I/O exception
449
     */
450
    public final String determineParent() throws IOException {
451
        return determineParent(CommandTimeoutType.INDEXER);
1✔
452
    }
453

454
    /**
455
     * Determine branch of this repository.
456
     */
457
    abstract String determineBranch(CommandTimeoutType cmdType) throws IOException;
458

459
    /**
460
     * Determine branch of this repository.
461
     * @return branch
462
     * @throws java.io.IOException I/O exception
463
     */
464
    public final String determineBranch() throws IOException {
465
        return determineBranch(CommandTimeoutType.INDEXER);
1✔
466
    }
467

468
    /**
469
     * Get list of ignored files for this repository.
470
     * @return list of strings
471
     */
472
    public List<String> getIgnoredFiles() {
473
        return ignoredFiles;
1✔
474
    }
475

476
    /**
477
     * Get list of ignored directories for this repository.
478
     * @return list of strings
479
     */
480
    public List<String> getIgnoredDirs() {
481
        return ignoredDirs;
1✔
482
    }
483

484
    /**
485
     * Determine and return the current version of the repository.
486
     * <p>
487
     * This operation is considered "heavy" so this function should not be
488
     * called on every web request.
489
     * </p>
490
     * @param cmdType command timeout type
491
     * @return the version
492
     * @throws IOException if I/O exception occurred
493
     */
494
    abstract String determineCurrentVersion(CommandTimeoutType cmdType) throws IOException;
495

496
    public final String determineCurrentVersion() throws IOException {
497
        return determineCurrentVersion(CommandTimeoutType.INDEXER);
1✔
498
    }
499

500
    /**
501
     * Returns true if this repository supports sub repositories (a.k.a.
502
     * forests).
503
     *
504
     * @return true if this repository supports sub repositories
505
     */
506
    @SuppressWarnings("PMD.EmptyMethodInAbstractClassShouldBeAbstract")
507
    boolean supportsSubRepositories() {
508
        return false;
1✔
509
    }
510

511
    /**
512
     * Subclasses can override to get a value indicating that a repository implementation is nestable.
513
     * @return {@code false}
514
     */
515
    boolean isNestable() {
516
        return false;
1✔
517
    }
518

519
    private DateFormat getDateFormat() {
520
        return new RepositoryDateFormat();
1✔
521
    }
522

523
    /**
524
     * Format the given date according to the output format.
525
     *
526
     * @param date the date
527
     * @return the string representing the formatted date
528
     * @see #OUTPUT_DATE_FORMAT
529
     */
530
    public static String format(Date date) {
531
        synchronized (OUTPUT_DATE_FORMAT) {
1✔
532
            return OUTPUT_DATE_FORMAT.format(date);
1✔
533
        }
534
    }
535

536
    /**
537
     * Parse the given string as a date object with the repository date formats.
538
     *
539
     * @param dateString the string representing the date
540
     * @return the instance of a date
541
     * @throws ParseException when the string can not be parsed correctly
542
     */
543
    public Date parse(String dateString) throws ParseException {
544
        final DateFormat format = getDateFormat();
1✔
545
        synchronized (format) {
1✔
546
            return format.parse(dateString);
1✔
547
        }
548
    }
549

550
    static Boolean checkCmd(String... args) {
551
        Executor exec = new Executor(args);
1✔
552
        return exec.exec(false) == 0;
1✔
553
    }
554

555
    protected static String getCommand(Class<? extends Repository> repoClass, String propertyKey, String fallbackCommand) {
556
        RuntimeEnvironment env = RuntimeEnvironment.getInstance();
1✔
557
        String className = repoClass.getCanonicalName();
1✔
558
        String command = env.getRepoCmd(className);
1✔
559
        if (command == null) {
1✔
560
            command = System.getProperty(propertyKey, fallbackCommand);
1✔
561
            env.setRepoCmd(className, command);
1✔
562
        }
563
        return command;
1✔
564
    }
565

566
    /**
567
     * Set the name of the external client command that should be used to access
568
     * the repository wrt. the given parameters. Does nothing, if this
569
     * repository's <var>RepoCommand</var> has already been set (i.e. has a non-{@code null} value).
570
     *
571
     * @param propertyKey property key to look up the corresponding system property.
572
     * @param fallbackCommand the command to use, if lookup fails.
573
     * @return the command to use.
574
     * @see #RepoCommand
575
     */
576
    protected String ensureCommand(String propertyKey, String fallbackCommand) {
577
        if (RepoCommand == null) {
1✔
578
            RepoCommand = getCommand(this.getClass(), propertyKey, fallbackCommand);
1✔
579
        }
580

581
        return RepoCommand;
1✔
582
    }
583

584
    /**
585
     * @param file File object
586
     * @return relative path to repository root
587
     * @throws IOException on I/O error
588
     */
589
    protected String getRepoRelativePath(final File file) throws IOException {
590
        String filename = file.getPath();
1✔
591
        String repoDirName = getDirectoryName();
1✔
592

593
        String canonicalPath = file.getCanonicalPath();
1✔
594
        if (canonicalPath.startsWith(repoDirName + File.separator)) {
1✔
595
            if (canonicalPath.length() > repoDirName.length()) {
1✔
596
                filename = canonicalPath.substring(repoDirName.length() + 1);
1✔
597
            }
598
        } else {
599
            String absolutePath = file.getAbsolutePath();
1✔
600
            if (absolutePath.startsWith(repoDirName + File.separator) &&
1✔
601
                    absolutePath.length() > repoDirName.length()) {
×
602
                filename = absolutePath.substring(repoDirName.length() + 1);
×
603
            }
604
        }
605

606
        return filename;
1✔
607
    }
608

609
    /**
610
     * Copies all bytes from {@code in} to the {@code sink}.
611
     * @return the number of writes to {@code sink}
612
     */
613
    static int copyBytes(BufferSink sink, InputStream in) throws IOException {
614
        byte[] buffer = new byte[8 * 1024];
1✔
615
        int iterations = 0;
1✔
616
        int len;
617
        while ((len = in.read(buffer)) != -1) {
1✔
618
            if (len > 0) {
1✔
619
                ++iterations;
1✔
620
                sink.write(buffer, 0, len);
1✔
621
            }
622
        }
623
        return iterations;
1✔
624
    }
625

626
    static class HistoryRevResult {
1✔
627
        boolean success;
628
        long iterations;
629
    }
630

631
    private class RepositoryDateFormat extends DateFormat {
1✔
632
        private static final long serialVersionUID = -6951382723884436414L;
633

634
        private final Locale locale = Locale.ENGLISH;
1✔
635
        // NOTE: SimpleDateFormat is not thread-safe, lock must be held when used
636
        private final SimpleDateFormat[] formatters = new SimpleDateFormat[datePatterns.length];
1✔
637

638
        {
639
            // initialize date formatters
640
            for (int i = 0; i < datePatterns.length; i++) {
1✔
641
                formatters[i] = new SimpleDateFormat(datePatterns[i], locale);
1✔
642
                /*
643
                 * TODO: the following would be nice - but currently it
644
                 * could break the compatibility with some repository dates
645
                 */
646
                // formatters[i].setLenient(false);
647
            }
648
        }
1✔
649

650
        @Override
651
        public StringBuffer format(Date date, StringBuffer toAppendTo, FieldPosition fieldPosition) {
652
            throw new UnsupportedOperationException("not implemented");
×
653
        }
654

655
        @Override
656
        public Date parse(String source) throws ParseException {
657
            ParseException head = null;
1✔
658
            ParseException tail = null;
1✔
659
            for (SimpleDateFormat formatter : formatters) {
1✔
660
                try {
661
                    return formatter.parse(source);
1✔
662
                } catch (ParseException ex1) {
1✔
663
                    /*
664
                     * Adding all exceptions together to get some info in
665
                     * the logs.
666
                     */
667
                    ex1 = new ParseException(
1✔
668
                            String.format("%s with format \"%s\" and locale \"%s\"",
1✔
669
                                    ex1.getMessage(),
1✔
670
                                    formatter.toPattern(),
1✔
671
                                    locale),
672
                            ex1.getErrorOffset()
1✔
673
                    );
674
                    if (head == null) {
1✔
675
                        head = tail = ex1;
1✔
676
                    } else {
677
                        tail.initCause(ex1);
1✔
678
                        tail = ex1;
1✔
679
                    }
680
                }
681
            }
682
            throw head != null ? head : new ParseException(String.format("Unparseable date: \"%s\"", source), 0);
1✔
683
        }
684

685
        @Override
686
        public Date parse(String source, ParsePosition pos) {
687
            throw new UnsupportedOperationException("not implemented");
×
688
        }
689
    }
690
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc