View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      https://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  
18  package org.apache.commons.net.nntp;
19  
20  import java.io.PrintStream;
21  import java.util.ArrayList;
22  import java.util.Collections;
23  
24  import org.apache.commons.net.util.NetConstants;
25  
26  /**
27   * Basic state needed for message retrieval and threading. With thanks to Jamie Zawinski (jwz@jwz.org)
28   */
29  public class Article implements Threadable<Article> {
30  
31      /**
32       * Recursive method that traverses a pre-threaded graph (or tree) of connected Article objects and prints them out.
33       *
34       * @param article the root of the article 'tree'
35       * @since 3.4
36       */
37      public static void printThread(final Article article) {
38          printThread(article, 0, System.out);
39      }
40  
41      /**
42       * Recursive method that traverses a pre-threaded graph (or tree) of connected Article objects and prints them out.
43       *
44       * @param article the root of the article 'tree'
45       * @param depth   the current tree depth
46       */
47      public static void printThread(final Article article, final int depth) {
48          printThread(article, depth, System.out);
49      }
50  
51      /**
52       * Recursive method that traverses a pre-threaded graph (or tree) of connected Article objects and prints them out.
53       *
54       * @param article the root of the article 'tree'
55       * @param depth   the current tree depth
56       * @param ps      the PrintStream to use
57       * @since 3.4
58       */
59      public static void printThread(final Article article, final int depth, final PrintStream ps) {
60          for (int i = 0; i < depth; ++i) {
61              ps.print("==>");
62          }
63          ps.println(article.getSubject() + "\t" + article.getFrom() + "\t" + article.getArticleId());
64          if (article.kid != null) {
65              printThread(article.kid, depth + 1);
66          }
67          if (article.next != null) {
68              printThread(article.next, depth);
69          }
70      }
71  
72      /**
73       * Recursive method that traverses a pre-threaded graph (or tree) of connected Article objects and prints them out.
74       *
75       * @param article the root of the article 'tree'
76       * @param ps      the PrintStream to use
77       * @since 3.4
78       */
79      public static void printThread(final Article article, final PrintStream ps) {
80          printThread(article, 0, ps);
81      }
82  
83      private long articleNumber;
84      private String subject;
85      private String date;
86      private String articleId;
87  
88      private String simplifiedSubject;
89  
90      private String from;
91      private ArrayList<String> references;
92  
93      private boolean isReply;
94  
95      /**
96       * Will be private in 4.0.
97       *
98       * @deprecated Use {@link #setChild(Article)} and {@link #getChild()}.
99       */
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 }