    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}