xref: /OpenGrok/opengrok-indexer/src/main/java/org/opengrok/indexer/history/SCCSHistoryParser.java (revision cf0c35f46431c17c500452aa2f94fc83af917e12)
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) 2007, 2022, Oracle and/or its affiliates. All rights reserved.
22  */
23 package org.opengrok.indexer.history;
24 
25 import java.io.BufferedReader;
26 import java.io.File;
27 import java.io.FileReader;
28 import java.io.IOException;
29 import java.io.Reader;
30 import java.nio.file.Paths;
31 import java.text.ParseException;
32 import java.util.ArrayList;
33 import java.util.Date;
34 
35 import org.jetbrains.annotations.Nullable;
36 import org.opengrok.indexer.util.IOUtils;
37 
38 /**
39  * Reads and filters out junk from a SCCS history file.
40  * See sccsfile(4) for details of the file format.
41  * Wrote it since invoking {@code sccs prs} for each file was
42  * taking a lot of time. Time to index history has reduced 4 to 1!
43  */
44 final class SCCSHistoryParser {
45 
46     static final String SCCS_DIR_NAME = "SCCS";
47 
48     boolean pass;
49     boolean passRecord;
50     boolean active;
51     int field;
52     boolean sep;
53     StringBuilder sccsRecord = new StringBuilder(128);
54     Reader in;
55 
56     // Record fields
57     private String revision;
58     private Date rdate;
59     private String author;
60     private String comment;
61     private final SCCSRepository repository;
62 
SCCSHistoryParser(SCCSRepository repository)63     SCCSHistoryParser(SCCSRepository repository) {
64         this.repository = repository;
65     }
66 
parse(File file)67     History parse(File file) throws HistoryException {
68         try {
69             return parseFile(file);
70         } catch (IOException e) {
71             throw new HistoryException("Failed to get history for " +
72                 "\"" + file.getAbsolutePath() + "\":", e);
73         } catch (ParseException e) {
74             throw new HistoryException("Failed to parse history for " +
75                 "\"" + file.getAbsolutePath() + "\":", e);
76         }
77     }
78 
parseFile(File file)79     private History parseFile(File file) throws IOException, ParseException {
80         File f = getSCCSFile(file);
81         if (f == null) {
82             return null;
83         }
84 
85         in = new BufferedReader(new FileReader(getSCCSFile(file)));
86         pass = sep = false;
87         passRecord = true;
88         active = true;
89         field = 0;
90 
91         ArrayList<HistoryEntry> entries = new ArrayList<>();
92         while (next()) {
93             HistoryEntry entry = new HistoryEntry();
94             entry.setRevision(getRevision());
95             entry.setDate(getDate());
96             entry.setAuthor(getAuthor());
97             entry.setMessage(getComment());
98             entry.setActive(isActive());
99             entries.add(entry);
100         }
101 
102         IOUtils.close(in);
103 
104         History history = new History();
105         history.setHistoryEntries(entries);
106         return history;
107     }
108 
109     /**
110      * Read a single line of delta record into the {@link #sccsRecord} member.
111      *
112      * @throws java.io.IOException on I/O error
113      * @return boolean indicating whether there is another record.
114      */
next()115     private boolean next() throws java.io.IOException {
116         sep = true;
117         sccsRecord.setLength(0);
118         int c;
119         while ((c = read()) > 1) {
120             sccsRecord.append((char) c);
121         }
122         // to flag that revision needs to be re populated if you really need it
123         revision = null;
124         return (sccsRecord.length() > 2);
125     }
126 
127     /**
128      * Split record into fields.
129      *
130      * @throws ParseException if the date in the record cannot be parsed
131      */
initFields()132     private void initFields() throws ParseException {
133         if (revision == null) {
134             String[] f = sccsRecord.toString().split(" ", 6);
135             if (f.length > 5) {
136                 revision = f[1];
137                 try {
138                     rdate = repository.parse(f[2] + " " + f[3]);
139                 } catch (ParseException e) {
140                     rdate = null;
141                     //
142                     // Throw new exception up so that it can be paired with filename
143                     // on which the problem occurred.
144                     //
145                     throw e;
146                 }
147                 author = f[4];
148                 comment = f[5];
149             } else {
150                 rdate = null;
151                 author = null;
152                 comment = null;
153             }
154         }
155     }
156 
157     /**
158      * @return  get the revision string of current log record
159      */
getRevision()160     private String getRevision() throws ParseException {
161         initFields();
162         return revision;
163     }
164 
165     /**
166      * @return  get the date associated with current log record
167      */
getDate()168     private Date getDate() throws ParseException {
169         initFields();
170         return rdate;
171     }
172 
173     /**
174      * @return  get the author of current log record
175      */
getAuthor()176     private String getAuthor() throws ParseException {
177         initFields();
178         return author;
179     }
180     /**
181      * @return  get the comments of current log record
182      */
getComment()183     private String getComment() throws ParseException {
184         initFields();
185         return comment;
186     }
187 
isActive()188     private boolean isActive() {
189         return active;
190     }
191 
read()192     private int read() throws java.io.IOException {
193         int c, d, dt;
194         while ((c = in.read()) != -1) {
195             switch (c) { //NOPMD
196                 case 1:
197                     d = in.read();
198                     switch (d) {
199                         case 'c':
200                         case 't':
201                         case 'u':
202                             d = in.read();
203                             if (d != ' ') {
204                                 return (d);
205                             }
206                             pass = true;
207                             break;
208                         case 'd':
209                             d = in.read();
210                             if (d == ' ') {
211                                 dt = in.read();
212                                 active = dt != 'R';
213                                 passRecord = true;
214                                 field = 1;
215                             } else {
216                                 return (d);
217                             }
218                             break;
219                         case -1:
220                         case 'I':   //the file contents start
221                         case 'D':
222                         case 'E':
223                         case 'T':
224                             return -1;
225                         case 'e':
226                             pass = false;
227                             if (sep && passRecord) {
228                                 return 1;
229                             }
230                             passRecord = true;
231                             break;
232                         default:
233                             pass = false;
234                     }
235                     break;
236                 case ' ':
237                     if (passRecord) {
238                         if (field > 0) {
239                             field++;
240                             pass = true;
241                         }
242                         if (field > 5) {
243                             field = 0;
244                             pass = false;
245                             return c;
246                         }
247                     }
248                 default:
249                     if (pass && passRecord) {
250                         return c;
251                     }
252             }
253         }
254         return -1;
255     }
256 
getSCCSFile(File file)257     private static File getSCCSFile(File file) {
258         return getSCCSFile(file.getParent(), file.getName());
259     }
260 
261     @Nullable
getSCCSFile(String parent, String name)262     static File getSCCSFile(String parent, String name) {
263         File f = Paths.get(parent, SCCS_DIR_NAME, "s." + name).toFile();
264         if (!f.exists()) {
265             return null;
266         }
267         return f;
268     }
269 }
270