001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      https://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.net.nntp;
019
020import java.io.PrintStream;
021import java.util.ArrayList;
022import java.util.Collections;
023
024import org.apache.commons.net.util.NetConstants;
025
026/**
027 * Basic state needed for message retrieval and threading. With thanks to Jamie Zawinski (jwz@jwz.org)
028 */
029public class Article implements Threadable<Article> {
030
031    /**
032     * Recursive method that traverses a pre-threaded graph (or tree) of connected Article objects and prints them out.
033     *
034     * @param article the root of the article 'tree'
035     * @since 3.4
036     */
037    public static void printThread(final Article article) {
038        printThread(article, 0, System.out);
039    }
040
041    /**
042     * Recursive method that traverses a pre-threaded graph (or tree) of connected Article objects and prints them out.
043     *
044     * @param article the root of the article 'tree'
045     * @param depth   the current tree depth
046     */
047    public static void printThread(final Article article, final int depth) {
048        printThread(article, depth, System.out);
049    }
050
051    /**
052     * Recursive method that traverses a pre-threaded graph (or tree) of connected Article objects and prints them out.
053     *
054     * @param article the root of the article 'tree'
055     * @param depth   the current tree depth
056     * @param ps      the PrintStream to use
057     * @since 3.4
058     */
059    public static void printThread(final Article article, final int depth, final PrintStream ps) {
060        for (int i = 0; i < depth; ++i) {
061            ps.print("==>");
062        }
063        ps.println(article.getSubject() + "\t" + article.getFrom() + "\t" + article.getArticleId());
064        if (article.kid != null) {
065            printThread(article.kid, depth + 1);
066        }
067        if (article.next != null) {
068            printThread(article.next, depth);
069        }
070    }
071
072    /**
073     * Recursive method that traverses a pre-threaded graph (or tree) of connected Article objects and prints them out.
074     *
075     * @param article the root of the article 'tree'
076     * @param ps      the PrintStream to use
077     * @since 3.4
078     */
079    public static void printThread(final Article article, final PrintStream ps) {
080        printThread(article, 0, ps);
081    }
082
083    private long articleNumber;
084    private String subject;
085    private String date;
086    private String articleId;
087
088    private String simplifiedSubject;
089
090    private String from;
091    private ArrayList<String> references;
092
093    private boolean isReply;
094
095    /**
096     * Will be private in 4.0.
097     *
098     * @deprecated Use {@link #setChild(Article)} and {@link #getChild()}.
099     */
100    @Deprecated
101    public Article kid;
102
103    /**
104     * Will be private in 4.0.
105     *
106     * @deprecated Use {@link #setNext(Article)} and {@link #getNext()}.
107     */
108    @Deprecated
109    public Article next;
110
111    /**
112     * Constructs a new instance.
113     */
114    public Article() {
115        articleNumber = -1; // isDummy
116    }
117
118    /**
119     * Does nothing.
120     *
121     * @param name Ignored.
122     * @param val  Ignored.
123     */
124    @Deprecated
125    public void addHeaderField(final String name, final String val) {
126        // empty
127    }
128
129    /**
130     * Adds a message-id to the list of messages that this message references (i.e. replies to)
131     *
132     * @param msgId the message id to add
133     */
134    public void addReference(final String msgId) {
135        if (msgId == null || msgId.isEmpty()) {
136            return;
137        }
138        if (references == null) {
139            references = new ArrayList<>();
140        }
141        isReply = true;
142        Collections.addAll(references, msgId.split(" "));
143    }
144
145    private void flushSubjectCache() {
146        simplifiedSubject = null;
147    }
148
149    /**
150     * Gets the article ID.
151     *
152     * @return the article ID.
153     */
154    public String getArticleId() {
155        return articleId;
156    }
157
158    /**
159     * Gets the article number.
160     *
161     * @return the article number.
162     */
163    @Deprecated
164    public int getArticleNumber() {
165        return (int) articleNumber;
166    }
167
168    /**
169     * Gets the article number.
170     *
171     * @return the article number.
172     */
173    public long getArticleNumberLong() {
174        return articleNumber;
175    }
176
177    /**
178     * Gets the child article.
179     *
180     * @return the child article.
181     * @since 3.12.0
182     */
183    public Article getChild() {
184        return kid;
185    }
186
187    /**
188     * Gets the article date header.
189     *
190     * @return the article date header.
191     */
192    public String getDate() {
193        return date;
194    }
195
196    /**
197     * Gets the article from header.
198     *
199     * @return the article from header.
200     */
201    public String getFrom() {
202        return from;
203    }
204
205    /**
206     * Gets the next article.
207     *
208     * @return the next article.
209     * @since 3.12.0
210     */
211    public Article getNext() {
212        return next;
213    }
214
215    /**
216     * Returns the MessageId references as an array of Strings
217     *
218     * @return an array of message-ids
219     */
220    public String[] getReferences() {
221        if (references == null) {
222            return NetConstants.EMPTY_STRING_ARRAY;
223        }
224        return references.toArray(NetConstants.EMPTY_STRING_ARRAY);
225    }
226
227    /**
228     * Gets the article subject.
229     *
230     * @return the article subject.
231     */
232    public String getSubject() {
233        return subject;
234    }
235
236    @Override
237    public boolean isDummy() {
238        return articleNumber == -1;
239    }
240
241    @Override
242    public Article makeDummy() {
243        return new Article();
244    }
245
246    @Override
247    public String messageThreadId() {
248        return articleId;
249    }
250
251    @Override
252    public String[] messageThreadReferences() {
253        return getReferences();
254    }
255
256    /**
257     * Sets the article ID.
258     *
259     * @param string the article ID.
260     */
261    public void setArticleId(final String string) {
262        articleId = string;
263    }
264
265    /**
266     * Sets the article number.
267     *
268     * @param articleNumber  the article number.
269     */
270    @Deprecated
271    public void setArticleNumber(final int articleNumber) {
272        this.articleNumber = articleNumber;
273    }
274
275    /**
276     * Sets the article number.
277     *
278     * @param articleNumber  the article number.
279     */
280    public void setArticleNumber(final long articleNumber) {
281        this.articleNumber = articleNumber;
282    }
283
284    @Override
285    public void setChild(final Article child) {
286        this.kid = child;
287        flushSubjectCache();
288    }
289
290    /**
291     * Sets the article date header.
292     *
293     * @param date  the article date header.
294     */
295    public void setDate(final String date) {
296        this.date = date;
297    }
298
299    /**
300     * Sets the article from header.
301     *
302     * @param from  the article from header.
303     */
304    public void setFrom(final String from) {
305        this.from = from;
306    }
307
308    @Override
309    public void setNext(final Article next) {
310        this.next = next;
311        flushSubjectCache();
312    }
313
314    /**
315     * Sets the article subject.
316     *
317     * @param subject  the article subject.
318     */
319    public void setSubject(final String subject) {
320        this.subject = subject;
321    }
322
323    @Override
324    public String simplifiedSubject() {
325        if (simplifiedSubject == null) {
326            simplifySubject();
327        }
328        return simplifiedSubject;
329    }
330
331    /**
332     * Attempts to parse the subject line for some typical reply signatures, and strip them out
333     */
334    private void simplifySubject() {
335        int start = 0;
336        final String subject = getSubject();
337        final int len = subject.length();
338
339        boolean done = false;
340
341        while (!done) {
342            done = true;
343
344            // skip whitespace
345            // "Re: " breaks this
346            while (start < len && subject.charAt(start) == ' ') {
347                start++;
348            }
349
350            if (start < len - 2 && (subject.charAt(start) == 'r' || subject.charAt(start) == 'R')
351                    && (subject.charAt(start + 1) == 'e' || subject.charAt(start + 1) == 'E')) {
352
353                if (subject.charAt(start + 2) == ':') {
354                    start += 3; // Skip "Re:"
355                    done = false;
356                } else if (start < len - 2 && (subject.charAt(start + 2) == '[' || subject.charAt(start + 2) == '(')) {
357
358                    int i = start + 3;
359
360                    while (i < len && subject.charAt(i) >= '0' && subject.charAt(i) <= '9') {
361                        i++;
362                    }
363
364                    if (i < len - 1 && (subject.charAt(i) == ']' || subject.charAt(i) == ')') && subject.charAt(i + 1) == ':') {
365                        start = i + 2;
366                        done = false;
367                    }
368                }
369            }
370
371            if ("(no subject)".equals(simplifiedSubject)) {
372                simplifiedSubject = "";
373            }
374
375            int end = len;
376
377            while (end > start && subject.charAt(end - 1) < ' ') {
378                end--;
379            }
380
381            if (start == 0 && end == len) {
382                simplifiedSubject = subject;
383            } else {
384                simplifiedSubject = subject.substring(start, end);
385            }
386        }
387    }
388
389    @Override
390    public boolean subjectIsReply() {
391        return isReply;
392    }
393
394    @Override
395    public String toString() { // Useful for Eclipse debugging
396        return articleNumber + " " + articleId + " " + subject;
397    }
398
399}