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 *      http://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 * This is a class that contains the basic state needed for message retrieval and threading. With thanks to Jamie Zawinski (jwz@jwz.org)
028 */
029public class Article implements Threadable {
030    /**
031     * Recursive method that traverses a pre-threaded graph (or tree) of connected Article objects and prints them out.
032     *
033     * @param article the root of the article 'tree'
034     * @since 3.4
035     */
036    public static void printThread(final Article article) {
037        printThread(article, 0, System.out);
038    }
039
040    /**
041     * Recursive method that traverses a pre-threaded graph (or tree) of connected Article objects and prints them out.
042     *
043     * @param article the root of the article 'tree'
044     * @param depth   the current tree depth
045     */
046    public static void printThread(final Article article, final int depth) {
047        printThread(article, depth, System.out);
048    }
049
050    /**
051     * Recursive method that traverses a pre-threaded graph (or tree) of connected Article objects and prints them out.
052     *
053     * @param article the root of the article 'tree'
054     * @param depth   the current tree depth
055     * @param ps      the PrintStream to use
056     * @since 3.4
057     */
058    public static void printThread(final Article article, final int depth, final PrintStream ps) {
059        for (int i = 0; i < depth; ++i) {
060            ps.print("==>");
061        }
062        ps.println(article.getSubject() + "\t" + article.getFrom() + "\t" + article.getArticleId());
063        if (article.kid != null) {
064            printThread(article.kid, depth + 1);
065        }
066        if (article.next != null) {
067            printThread(article.next, depth);
068        }
069    }
070
071    /**
072     * Recursive method that traverses a pre-threaded graph (or tree) of connected Article objects and prints them out.
073     *
074     * @param article the root of the article 'tree'
075     * @param ps      the PrintStream to use
076     * @since 3.4
077     */
078    public static void printThread(final Article article, final PrintStream ps) {
079        printThread(article, 0, ps);
080    }
081
082    private long articleNumber;
083    private String subject;
084    private String date;
085    private String articleId;
086
087    private String simplifiedSubject;
088
089    private String from;
090    private ArrayList<String> references;
091
092    private boolean isReply;
093
094    public Article kid, next;
095
096    public Article() {
097        articleNumber = -1; // isDummy
098    }
099
100    @Deprecated
101
102    public void addHeaderField(final String name, final String val) {
103    }
104
105    /**
106     * Adds a message-id to the list of messages that this message references (i.e. replies to)
107     *
108     * @param msgId the message id to add
109     */
110    public void addReference(final String msgId) {
111        if (msgId == null || msgId.isEmpty()) {
112            return;
113        }
114        if (references == null) {
115            references = new ArrayList<>();
116        }
117        isReply = true;
118        Collections.addAll(references, msgId.split(" "));
119    }
120
121    private void flushSubjectCache() {
122        simplifiedSubject = null;
123    }
124
125    public String getArticleId() {
126        return articleId;
127    }
128
129    @Deprecated
130    public int getArticleNumber() {
131        return (int) articleNumber;
132    }
133
134    public long getArticleNumberLong() {
135        return articleNumber;
136    }
137
138    public String getDate() {
139        return date;
140    }
141
142    public String getFrom() {
143        return from;
144    }
145
146    /**
147     * Returns the MessageId references as an array of Strings
148     *
149     * @return an array of message-ids
150     */
151    public String[] getReferences() {
152        if (references == null) {
153            return NetConstants.EMPTY_STRING_ARRAY;
154        }
155        return references.toArray(NetConstants.EMPTY_STRING_ARRAY);
156    }
157
158    public String getSubject() {
159        return subject;
160    }
161
162    @Override
163    public boolean isDummy() {
164        return articleNumber == -1;
165    }
166
167    @Override
168    public Threadable makeDummy() {
169        return new Article();
170    }
171
172    @Override
173    public String messageThreadId() {
174        return articleId;
175    }
176
177    @Override
178    public String[] messageThreadReferences() {
179        return getReferences();
180    }
181
182    public void setArticleId(final String string) {
183        articleId = string;
184    }
185
186    @Deprecated
187    public void setArticleNumber(final int a) {
188        articleNumber = a;
189    }
190
191    public void setArticleNumber(final long l) {
192        articleNumber = l;
193    }
194
195    @Override
196    public void setChild(final Threadable child) {
197        this.kid = (Article) child;
198        flushSubjectCache();
199    }
200
201    public void setDate(final String string) {
202        date = string;
203    }
204
205    public void setFrom(final String string) {
206        from = string;
207    }
208
209    @Override
210    public void setNext(final Threadable next) {
211        this.next = (Article) next;
212        flushSubjectCache();
213    }
214
215    public void setSubject(final String string) {
216        subject = string;
217    }
218
219    @Override
220    public String simplifiedSubject() {
221        if (simplifiedSubject == null) {
222            simplifySubject();
223        }
224        return simplifiedSubject;
225    }
226
227    // DEPRECATED METHODS - for API compatibility only - DO NOT USE
228
229    /**
230     * Attempts to parse the subject line for some typical reply signatures, and strip them out
231     *
232     */
233    private void simplifySubject() {
234        int start = 0;
235        final String subject = getSubject();
236        final int len = subject.length();
237
238        boolean done = false;
239
240        while (!done) {
241            done = true;
242
243            // skip whitespace
244            // "Re: " breaks this
245            while (start < len && subject.charAt(start) == ' ') {
246                start++;
247            }
248
249            if (start < len - 2 && (subject.charAt(start) == 'r' || subject.charAt(start) == 'R')
250                    && (subject.charAt(start + 1) == 'e' || subject.charAt(start + 1) == 'E')) {
251
252                if (subject.charAt(start + 2) == ':') {
253                    start += 3; // Skip "Re:"
254                    done = false;
255                } else if (start < len - 2 && (subject.charAt(start + 2) == '[' || subject.charAt(start + 2) == '(')) {
256
257                    int i = start + 3;
258
259                    while (i < len && subject.charAt(i) >= '0' && subject.charAt(i) <= '9') {
260                        i++;
261                    }
262
263                    if (i < len - 1 && (subject.charAt(i) == ']' || subject.charAt(i) == ')') && subject.charAt(i + 1) == ':') {
264                        start = i + 2;
265                        done = false;
266                    }
267                }
268            }
269
270            if ("(no subject)".equals(simplifiedSubject)) {
271                simplifiedSubject = "";
272            }
273
274            int end = len;
275
276            while (end > start && subject.charAt(end - 1) < ' ') {
277                end--;
278            }
279
280            if (start == 0 && end == len) {
281                simplifiedSubject = subject;
282            } else {
283                simplifiedSubject = subject.substring(start, end);
284            }
285        }
286    }
287
288    @Override
289    public boolean subjectIsReply() {
290        return isReply;
291    }
292
293    @Override
294    public String toString() { // Useful for Eclipse debugging
295        return articleNumber + " " + articleId + " " + subject;
296    }
297
298}