001package org.apache.maven.tools.plugin.javadoc;
002
003/*
004 * Licensed to the Apache Software Foundation (ASF) under one
005 * or more contributor license agreements.  See the NOTICE file
006 * distributed with this work for additional information
007 * regarding copyright ownership.  The ASF licenses this file
008 * to you under the Apache License, Version 2.0 (the
009 * "License"); you may not use this file except in compliance
010 * with the License.  You may obtain a copy of the License at
011 *
012 *   http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing,
015 * software distributed under the License is distributed on an
016 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
017 * KIND, either express or implied.  See the License for the
018 * specific language governing permissions and limitations
019 * under the License.
020 */
021
022import java.util.ArrayList;
023import java.util.Arrays;
024import java.util.Collections;
025import java.util.Enumeration;
026import java.util.List;
027import java.util.StringTokenizer;
028
029import javax.swing.text.AttributeSet;
030import javax.swing.text.MutableAttributeSet;
031import javax.swing.text.SimpleAttributeSet;
032
033import com.sun.javadoc.Tag;
034import com.sun.tools.doclets.Taglet;
035
036/**
037 * Abstract <code>Taglet</code> for <a href="http://maven.codehaus.org/"/>Maven</a> Mojo annotations.
038 * <br/>
039 * A Mojo annotation is defined like the following:
040 * <pre>
041 * &#64;annotation &lt;annotationValue&gt; &lt;parameterName="parameterValue"&gt;
042 * </pre>
043 *
044 * @see <a href="package-summary.html#package_description">package-summary.html</a>
045 *
046 * @author <a href="mailto:vincent.siveton@gmail.com">Vincent Siveton</a>
047 * @version $Id: AbstractMojoTaglet.html 1030109 2018-05-20 14:45:18Z hboutemy $
048 */
049public abstract class AbstractMojoTaglet
050    implements Taglet
051{
052    /** {@inheritDoc} */
053    public String toString( Tag tag )
054    {
055        if ( tag == null )
056        {
057            return null;
058        }
059
060        String tagValue = getTagValue( tag );
061        MutableAttributeSet tagAttributes = getTagAttributes( tag );
062
063        StringBuilder sb = new StringBuilder();
064
065        appendTag( sb, tag, tagAttributes, tagValue );
066
067        return sb.toString();
068    }
069
070    /** {@inheritDoc} */
071    public String toString( Tag[] tags )
072    {
073        if ( tags.length == 0 )
074        {
075            return null;
076        }
077
078        StringBuilder sb = new StringBuilder();
079        for ( int i = 0; i < tags.length; i++ )
080        {
081            String tagValue = getTagValue( tags[i] );
082            MutableAttributeSet tagAttributes = getTagAttributes( tags[i] );
083
084            appendTag( sb, tags[i], tagAttributes, tagValue );
085        }
086
087        return sb.toString();
088    }
089
090    /**
091     * @return the header, i.e. the message, to display
092     */
093    public abstract String getHeader();
094
095    /**
096     * @return the given annotation value, or <code>null</code> if the given Mojo annotation/tag does't allow
097     * annotation value.
098     * <br/>
099     * <b>Note</b>: the value could be a pattern value, i.e.: <code>*</code> for every values, <code>a|b|c</code>
100     * for <code>a OR b OR c</code>.
101     */
102    public abstract String getAllowedValue();
103
104    /**
105     * @return an array of the allowed parameter names for the given Mojo annotation/tag, or <code>null</code>
106     * if the annotation/tag doesn't allow parameter.
107     */
108    public abstract String[] getAllowedParameterNames();
109
110    /**
111     * @return <code>true</code> if taglet has annotation value, <code>false</code> otherwise.
112     * @see #getAllowedValue()
113     */
114    public boolean hasAnnotationValue()
115    {
116        return getAllowedValue() != null;
117    }
118
119    /**
120     * @return <code>true</code> if taglet has parameters, <code>false</code> otherwise.
121     * @see #getAllowedParameterNames()
122     */
123    public boolean hasAnnotationParameters()
124    {
125        return getAllowedParameterNames() != null;
126    }
127
128    /**
129     * @param tag not null.
130     * @return a not null String or <code>null</code> if no annotation value was found.
131     */
132    private String getTagValue( Tag tag )
133    {
134        if ( tag == null )
135        {
136            throw new IllegalArgumentException( "tag should be not null" );
137        }
138
139        String text = tag.text();
140        if ( isEmpty( text ) )
141        {
142            // using pattern: @annotation
143            return null;
144        }
145
146        String tagValue = null;
147        StringTokenizer token = new StringTokenizer( text, " " );
148        while ( token.hasMoreTokens() )
149        {
150            String nextToken = token.nextToken();
151
152            if ( nextToken.indexOf( '=' ) == -1 )
153            {
154                // using pattern: @annotation <annotationValue>
155                tagValue = nextToken;
156            }
157        }
158
159        return tagValue;
160    }
161
162    /**
163     * @param tag not null.
164     * @return a not null MutableAttributeSet.
165     */
166    private MutableAttributeSet getTagAttributes( Tag tag )
167    {
168        if ( tag == null )
169        {
170            throw new IllegalArgumentException( "tag should be not null" );
171        }
172
173        String text = tag.text();
174
175        StringTokenizer token = new StringTokenizer( text, " " );
176        MutableAttributeSet tagAttributes = new SimpleAttributeSet();
177        while ( token.hasMoreTokens() )
178        {
179            String nextToken = token.nextToken();
180
181            if ( nextToken.indexOf( '=' ) == -1 )
182            {
183                // using pattern: @annotation <annotationValue>
184                continue;
185            }
186
187            StringTokenizer token2 = new StringTokenizer( nextToken, "=" );
188            if ( token2.countTokens() != 2 )
189            {
190                System.err.println( "The annotation '" + tag.name() + "' has no name/value pairs parameter: "
191                    + tag.name() + " " + text + " (" + tag.position().file() + ":" + tag.position().line() + ":"
192                    + tag.position().column() + ")" );
193                tagAttributes.addAttribute( token2.nextToken(), "" );
194                continue;
195            }
196
197            String name = token2.nextToken();
198            String value = token2.nextToken().replaceAll( "\"", "" );
199
200            if ( getAllowedParameterNames() != null && !Arrays.asList( getAllowedParameterNames() ).contains( name ) )
201            {
202                System.err.println( "The annotation '" + tag.name() + "' has wrong parameter name: " + tag.name() + " "
203                    + text + " (" + tag.position().file() + ":" + tag.position().line() + ":" + tag.position().column()
204                    + ")" );
205            }
206
207            tagAttributes.addAttribute( name, value );
208        }
209
210        return tagAttributes;
211    }
212
213    /**
214     * Append a tag
215     *
216     * @param sb not null
217     * @param tag not null
218     * @param tagAttributes not null
219     * @param tagValue not null
220     */
221    private void appendTag( StringBuilder sb, Tag tag, MutableAttributeSet tagAttributes, String tagValue )
222    {
223        if ( !hasAnnotationParameters() )
224        {
225            if ( tagAttributes.getAttributeCount() > 0 )
226            {
227                System.err.println( "The annotation '@" + getName() + "' should have no attribute ("
228                    + tag.position().file() + ":" + tag.position().line() + ":" + tag.position().column() + ")" );
229            }
230
231            if ( hasAnnotationValue() )
232            {
233                sb.append( "<DT><B>" ).append( getHeader() ).append( ":</B></DT>" );
234                if ( isEveryValues( getAllowedValue() ) )
235                {
236                    if ( isNotEmpty( tagValue ) )
237                    {
238                        sb.append( "<DD>" ).append( tagValue ).append( "</DD>" );
239                    }
240                    else
241                    {
242                        System.err.println( "The annotation '@" + getName() + "' is specified to have a value but "
243                            + "no value is defined (" + tag.position().file() + ":" + tag.position().line() + ":"
244                            + tag.position().column() + ")" );
245                        sb.append( "<DD>" ).append( "NOT DEFINED" ).append( "</DD>" );
246                    }
247                }
248                else
249                {
250                    List<String> l = getOnlyValues( getAllowedValue() );
251                    if ( isNotEmpty( tagValue ) )
252                    {
253                        if ( l.contains( tagValue ) )
254                        {
255                            sb.append( "<DD>" ).append( tagValue ).append( "</DD>" );
256                        }
257                        else
258                        {
259                            System.err.println( "The annotation '@" + getName() + "' is specified to be a value of "
260                                + l + " (" + tag.position().file() + ":" + tag.position().line() + ":"
261                                + tag.position().column() + ")" );
262                            sb.append( "<DD>" ).append( tagValue ).append( "</DD>" );
263                        }
264                    }
265                    else
266                    {
267                        sb.append( "<DD>" ).append( l.get( 0 ) ).append( "</DD>" );
268                    }
269                }
270            }
271            else
272            {
273                if ( isNotEmpty( tagValue ) )
274                {
275                    System.err.println( "The annotation '@" + getName() + "' should have no value ("
276                        + tag.position().file() + ":" + tag.position().line() + ":" + tag.position().column() + ")" );
277                }
278                sb.append( "<DT><B>" ).append( getHeader() ).append( "</B></DT>" );
279                sb.append( "<DD></DD>" );
280            }
281        }
282        else
283        {
284            if ( hasAnnotationValue() )
285            {
286                sb.append( "<DT><B>" ).append( getHeader() ).append( ":</B></DT>" );
287                if ( isEveryValues( getAllowedValue() ) )
288                {
289                    if ( isNotEmpty( tagValue ) )
290                    {
291                        sb.append( "<DD>" ).append( tagValue );
292                    }
293                    else
294                    {
295                        System.err.println( "The annotation '@" + getName() + "' is specified to have a value but "
296                            + "no value is defined (" + tag.position().file() + ":" + tag.position().line() + ":"
297                            + tag.position().column() + ")" );
298                        sb.append( "<DD>" ).append( "NOT DEFINED" );
299                    }
300                }
301                else
302                {
303                    List<String> l = getOnlyValues( getAllowedValue() );
304                    if ( isNotEmpty( tagValue ) )
305                    {
306                        if ( l.contains( tagValue ) )
307                        {
308                            sb.append( "<DD>" ).append( tagValue );
309                        }
310                        else
311                        {
312                            System.err.println( "The annotation '@" + getName() + "' is specified to be a value in "
313                                + l + " (" + tag.position().file() + ":" + tag.position().line() + ":"
314                                + tag.position().column() + ")" );
315                            sb.append( "<DD>" ).append( tagValue );
316                        }
317                    }
318                    else
319                    {
320                        sb.append( "<DD>" ).append( l.get( 0 ) );
321                    }
322                }
323            }
324            else
325            {
326                if ( isNotEmpty( tagValue ) )
327                {
328                    System.err.println( "The annotation '@" + getName() + "' should have no value ("
329                        + tag.position().file() + ":" + tag.position().line() + ":" + tag.position().column() + ")" );
330                }
331                sb.append( "<DT><B>" ).append( getHeader() ).append( ":</B></DT>" );
332                sb.append( "<DD>" );
333            }
334
335            appendAnnotationParameters( sb, tagAttributes );
336            sb.append( "</DD>" );
337        }
338    }
339
340    /**
341     * Append the annotation parameters as a definition list.
342     *
343     * @param sb not null
344     * @param att not null
345     */
346    private static void appendAnnotationParameters( StringBuilder sb, MutableAttributeSet att )
347    {
348        sb.append( "<DL>" );
349
350        Enumeration<?> names = att.getAttributeNames();
351        while ( names.hasMoreElements() )
352        {
353            Object key = names.nextElement();
354            Object value = att.getAttribute( key );
355
356            if ( value instanceof AttributeSet )
357            {
358                // ignored
359            }
360            else
361            {
362                sb.append( "<DT><B>" ).append( key ).append( ":</B></DT>" );
363                sb.append( "<DD>" ).append( value ).append( "</DD>" );
364            }
365        }
366
367        sb.append( "</DL>" );
368    }
369
370    /**
371     * @param text not null
372     * @return <code>true</code> if text contains <code>*</code>, <code>false</code> otherwise.
373     */
374    private static boolean isEveryValues( String text )
375    {
376        return text.trim().equals( "*" );
377    }
378
379    /**
380     * Splits the provided text into a array, using pipe as the separator.
381     *
382     * @param text not null
383     * @return a list of parsed Strings or <code>Collections.EMPTY_LIST</code>.
384     * By convention, the default value is the first element.
385     */
386    private static List<String> getOnlyValues( String text )
387    {
388        if ( text.indexOf( '|' ) == -1 )
389        {
390            return Collections.emptyList();
391        }
392
393        List<String> l = new ArrayList<String>();
394        StringTokenizer token = new StringTokenizer( text, "|" );
395        while ( token.hasMoreTokens() )
396        {
397            l.add( token.nextToken() );
398        }
399
400        return l;
401    }
402
403    /**
404     * <p>Checks if a String is non <code>null</code> and is
405     * not empty (<code>length > 0</code>).</p>
406     *
407     * @param str the String to check
408     * @return true if the String is non-null, and not length zero
409     */
410    private static boolean isNotEmpty( String str )
411    {
412        return ( str != null && str.length() > 0 );
413    }
414
415    /**
416     * <p>Checks if a (trimmed) String is <code>null</code> or empty.</p>
417     *
418     * @param str the String to check
419     * @return <code>true</code> if the String is <code>null</code>, or
420     *  length zero once trimmed
421     */
422    private static boolean isEmpty( String str )
423    {
424        return ( str == null || str.trim().length() == 0 );
425    }
426}