001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *   http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.maven.scm.provider.svn.svnexe.command.changelog;
020
021import java.util.ArrayList;
022import java.util.Date;
023import java.util.List;
024import java.util.regex.Matcher;
025import java.util.regex.Pattern;
026
027import org.apache.maven.scm.ChangeFile;
028import org.apache.maven.scm.ChangeSet;
029import org.apache.maven.scm.ScmFileStatus;
030import org.apache.maven.scm.provider.svn.SvnChangeSet;
031import org.apache.maven.scm.util.AbstractConsumer;
032
033/**
034 * @author <a href="mailto:evenisse@apache.org">Emmanuel Venisse</a>
035 */
036public class SvnChangeLogConsumer extends AbstractConsumer {
037    /**
038     * Date formatter for svn timestamp (after a little massaging)
039     */
040    private static final String SVN_TIMESTAMP_PATTERN = "yyyy-MM-dd HH:mm:ss zzzzzzzzz";
041
042    /**
043     * State machine constant: expecting header.
044     */
045    private static final int GET_HEADER = 1;
046
047    /**
048     * State machine constant: expecting file information.
049     */
050    private static final int GET_FILE = 2;
051
052    /**
053     * State machine constant: expecting comments.
054     */
055    private static final int GET_COMMENT = 3;
056
057    /**
058     * There is always action and affected path; when copying/moving, recognize also original path and revision.
059     */
060    private static final Pattern FILE_PATTERN = Pattern.compile("^\\s\\s\\s([A-Z])\\s(.+)$");
061
062    /**
063     * This matches the 'original file info' part of the complete file line.
064     * Note the use of [:alpha:] instead of literal 'from' - this is meant to allow non-English localizations.
065     */
066    private static final Pattern ORIG_FILE_PATTERN = Pattern.compile("\\([A-Za-z]+ (.+):(\\d+)\\)");
067
068    /**
069     * The file section ends with a blank line.
070     */
071    private static final String FILE_END_TOKEN = "";
072
073    /**
074     * The comment section ends with a dashed line.
075     */
076    private static final String COMMENT_END_TOKEN =
077            "------------------------------------" + "------------------------------------";
078
079    /**
080     * Current status of the parser.
081     */
082    private int status = GET_HEADER;
083
084    /**
085     * List of change log entries.
086     */
087    private final List<ChangeSet> entries = new ArrayList<>();
088
089    /**
090     * The current log entry being processed by the parser.
091     */
092    private SvnChangeSet currentChange;
093
094    /**
095     * The current revision of the entry being processed by the parser.
096     */
097    private String currentRevision;
098
099    /**
100     * The current comment of the entry being processed by the parser.
101     */
102    private StringBuilder currentComment;
103
104    /**
105     * The regular expression used to match header lines.
106     */
107    private static final Pattern HEADER_REG_EXP = Pattern.compile("^(.+) \\| (.+) \\| (.+) \\|.*$");
108
109    private static final int REVISION_GROUP = 1;
110
111    private static final int AUTHOR_GROUP = 2;
112
113    private static final int DATE_GROUP = 3;
114
115    private static final Pattern REVISION_REG_EXP1 = Pattern.compile("rev (\\d+):");
116
117    private static final Pattern REVISION_REG_EXP2 = Pattern.compile("r(\\d+)");
118
119    private static final Pattern DATE_REG_EXP = Pattern.compile("(\\d+-\\d+-\\d+ " + // date 2002-08-24
120            "\\d+:\\d+:\\d+) "
121            + // time 16:01:00
122            "([\\-+])(\\d\\d)(\\d\\d)"); // gmt offset -0400);)
123
124    private final String userDateFormat;
125
126    /**
127     * Default constructor.
128     */
129    public SvnChangeLogConsumer(String userDateFormat) {
130        this.userDateFormat = userDateFormat;
131    }
132
133    public List<ChangeSet> getModifications() {
134        return entries;
135    }
136
137    // ----------------------------------------------------------------------
138    // StreamConsumer Implementation
139    // ----------------------------------------------------------------------
140
141    /**
142     * {@inheritDoc}
143     */
144    public void consumeLine(String line) {
145        if (logger.isDebugEnabled()) {
146            logger.debug(line);
147        }
148        switch (status) {
149            case GET_HEADER:
150                processGetHeader(line);
151                break;
152            case GET_FILE:
153                processGetFile(line);
154                break;
155            case GET_COMMENT:
156                processGetComment(line);
157                break;
158            default:
159                throw new IllegalStateException("Unknown state: " + status);
160        }
161    }
162
163    // ----------------------------------------------------------------------
164    //
165    // ----------------------------------------------------------------------
166
167    /**
168     * Process the current input line in the GET_HEADER state.  The
169     * author, date, and the revision of the entry are gathered.  Note,
170     * Subversion does not have per-file revisions, instead, the entire
171     * repository is given a single revision number, which is used for
172     * the revision number of each file.
173     *
174     * @param line a line of text from the svn log output
175     */
176    private void processGetHeader(String line) {
177        Matcher matcher = HEADER_REG_EXP.matcher(line);
178        if (!matcher.matches()) {
179            // The header line is not found. Intentionally do nothing.
180            return;
181        }
182
183        currentRevision = getRevision(matcher.group(REVISION_GROUP));
184
185        currentChange = new SvnChangeSet();
186
187        currentChange.setAuthor(matcher.group(AUTHOR_GROUP));
188
189        currentChange.setDate(getDate(matcher.group(DATE_GROUP)));
190
191        currentChange.setRevision(currentRevision);
192
193        status = GET_FILE;
194    }
195
196    /**
197     * Gets the svn revision, from the svn log revision output.
198     *
199     * @param revisionOutput
200     * @return the svn revision
201     */
202    private String getRevision(final String revisionOutput) {
203        Matcher matcher;
204        if ((matcher = REVISION_REG_EXP1.matcher(revisionOutput)).matches()) {
205            return matcher.group(1);
206        } else if ((matcher = REVISION_REG_EXP2.matcher(revisionOutput)).matches()) {
207            return matcher.group(1);
208        } else {
209            throw new IllegalOutputException(revisionOutput);
210        }
211    }
212
213    /**
214     * Process the current input line in the GET_FILE state.  This state
215     * adds each file entry line to the current change log entry.  Note,
216     * the revision number for the entire entry is used for the revision
217     * number of each file.
218     *
219     * @param line a line of text from the svn log output
220     */
221    private void processGetFile(String line) {
222        Matcher matcher = FILE_PATTERN.matcher(line);
223        if (matcher.matches()) {
224            final String fileinfo = matcher.group(2);
225            String name = fileinfo;
226            String originalName = null;
227            String originalRev = null;
228            final int n = fileinfo.indexOf(" (");
229            if (n > 1 && fileinfo.endsWith(")")) {
230                final String origFileInfo = fileinfo.substring(n);
231                Matcher matcher2 = ORIG_FILE_PATTERN.matcher(origFileInfo);
232                if (matcher2.find()) {
233                    // if original file is present, we must extract the affected one from the beginning
234                    name = fileinfo.substring(0, n);
235                    originalName = matcher2.group(1);
236                    originalRev = matcher2.group(2);
237                }
238            }
239            final String actionStr = matcher.group(1);
240            final ScmFileStatus action;
241            if ("A".equals(actionStr)) {
242                // TODO: this may even change to MOVED if we later explore whole changeset and find matching DELETED
243                action = originalRev == null ? ScmFileStatus.ADDED : ScmFileStatus.COPIED;
244            } else if ("D".equals(actionStr)) {
245                action = ScmFileStatus.DELETED;
246            } else if ("M".equals(actionStr)) {
247                action = ScmFileStatus.MODIFIED;
248            } else if ("R".equals(actionStr)) {
249                action = ScmFileStatus.UPDATED; // == REPLACED in svn terms
250            } else {
251                action = ScmFileStatus.UNKNOWN;
252            }
253            if (logger.isDebugEnabled()) {
254                logger.debug(actionStr + " : " + name);
255            }
256            final ChangeFile changeFile = new ChangeFile(name, currentRevision);
257            changeFile.setAction(action);
258            changeFile.setOriginalName(originalName);
259            changeFile.setOriginalRevision(originalRev);
260            currentChange.addFile(changeFile);
261
262            status = GET_FILE;
263        } else if (line.equals(FILE_END_TOKEN)) {
264            // Create a buffer for the collection of the comment now
265            // that we are leaving the GET_FILE state.
266            currentComment = new StringBuilder();
267
268            status = GET_COMMENT;
269        }
270    }
271
272    /**
273     * Process the current input line in the GET_COMMENT state.  This
274     * state gathers all of the comments that are part of a log entry.
275     *
276     * @param line a line of text from the svn log output
277     */
278    private void processGetComment(String line) {
279        if (line.equals(COMMENT_END_TOKEN)) {
280            currentChange.setComment(currentComment.toString());
281
282            entries.add(currentChange);
283
284            status = GET_HEADER;
285        } else {
286            currentComment.append(line).append('\n');
287        }
288    }
289
290    /**
291     * Converts the date time stamp from the svn output into a date
292     * object.
293     *
294     * @param dateOutput the date output from an svn log command
295     * @return a date representing the time stamp of the log entry
296     */
297    private Date getDate(final String dateOutput) {
298        Matcher matcher = DATE_REG_EXP.matcher(dateOutput);
299        if (!matcher.find()) {
300            throw new IllegalOutputException(dateOutput);
301        }
302
303        final StringBuilder date = new StringBuilder();
304        date.append(matcher.group(1));
305        date.append(" GMT");
306        date.append(matcher.group(2));
307        date.append(matcher.group(3));
308        date.append(':');
309        date.append(matcher.group(4));
310
311        return parseDate(date.toString(), userDateFormat, SVN_TIMESTAMP_PATTERN);
312    }
313}