001package org.apache.maven.model.interpolation;
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 org.apache.maven.model.Model;
023import org.apache.maven.model.building.ModelBuildingRequest;
024import org.apache.maven.model.building.ModelProblem.Severity;
025import org.apache.maven.model.building.ModelProblem.Version;
026import org.apache.maven.model.building.ModelProblemCollector;
027import org.apache.maven.model.building.ModelProblemCollectorRequest;
028import org.codehaus.plexus.component.annotations.Component;
029import org.codehaus.plexus.interpolation.InterpolationPostProcessor;
030import org.codehaus.plexus.interpolation.Interpolator;
031import org.codehaus.plexus.interpolation.StringSearchInterpolator;
032import org.codehaus.plexus.interpolation.ValueSource;
033
034import java.io.File;
035import java.lang.reflect.Array;
036import java.lang.reflect.Field;
037import java.lang.reflect.Modifier;
038import java.security.AccessController;
039import java.security.PrivilegedAction;
040import java.util.ArrayList;
041import java.util.Collection;
042import java.util.LinkedList;
043import java.util.List;
044import java.util.Map;
045import java.util.concurrent.ConcurrentHashMap;
046
047@Component( role = ModelInterpolator.class )
048public class StringSearchModelInterpolator
049    extends AbstractStringBasedModelInterpolator
050{
051
052    private static final Map<Class<?>, InterpolateObjectAction.CacheItem> CACHED_ENTRIES =
053        new ConcurrentHashMap<>( 80, 0.75f, 2 );
054    // Empirical data from 3.x, actual =40
055
056
057    @Override
058    public Model interpolateModel( Model model, File projectDir, ModelBuildingRequest config,
059                                   ModelProblemCollector problems )
060    {
061        interpolateObject( model, model, projectDir, config, problems );
062
063        return model;
064    }
065
066    protected void interpolateObject( Object obj, Model model, File projectDir, ModelBuildingRequest config,
067                                      ModelProblemCollector problems )
068    {
069        try
070        {
071            List<? extends ValueSource> valueSources = createValueSources( model, projectDir, config, problems );
072            List<? extends InterpolationPostProcessor> postProcessors =
073                createPostProcessors( model, projectDir, config );
074
075            InterpolateObjectAction action =
076                new InterpolateObjectAction( obj, valueSources, postProcessors, this, problems );
077
078            AccessController.doPrivileged( action );
079        }
080        finally
081        {
082            getInterpolator().clearAnswers();
083        }
084    }
085
086    @Override
087    protected Interpolator createInterpolator()
088    {
089        StringSearchInterpolator interpolator = new StringSearchInterpolator();
090        interpolator.setCacheAnswers( true );
091
092        return interpolator;
093    }
094
095    private static final class InterpolateObjectAction
096        implements PrivilegedAction<Object>
097    {
098
099        private final LinkedList<Object> interpolationTargets;
100
101        private final StringSearchModelInterpolator modelInterpolator;
102
103        private final List<? extends ValueSource> valueSources;
104
105        private final List<? extends InterpolationPostProcessor> postProcessors;
106
107        private final ModelProblemCollector problems;
108
109        public InterpolateObjectAction( Object target, List<? extends ValueSource> valueSources,
110                                        List<? extends InterpolationPostProcessor> postProcessors,
111                                        StringSearchModelInterpolator modelInterpolator,
112                                        ModelProblemCollector problems )
113        {
114            this.valueSources = valueSources;
115            this.postProcessors = postProcessors;
116
117            this.interpolationTargets = new LinkedList<>();
118            interpolationTargets.add( target );
119
120            this.modelInterpolator = modelInterpolator;
121
122            this.problems = problems;
123        }
124
125        @Override
126        public Object run()
127        {
128            while ( !interpolationTargets.isEmpty() )
129            {
130                Object obj = interpolationTargets.removeFirst();
131
132                traverseObjectWithParents( obj.getClass(), obj );
133            }
134
135            return null;
136        }
137
138
139        private String interpolate( String value )
140        {
141            return modelInterpolator.interpolateInternal( value, valueSources, postProcessors, problems );
142        }
143
144        private void traverseObjectWithParents( Class<?> cls, Object target )
145        {
146            if ( cls == null )
147            {
148                return;
149            }
150
151            CacheItem cacheEntry = getCacheEntry( cls );
152            if ( cacheEntry.isArray() )
153            {
154                evaluateArray( target, this );
155            }
156            else if ( cacheEntry.isQualifiedForInterpolation )
157            {
158                cacheEntry.interpolate( target, this );
159
160                traverseObjectWithParents( cls.getSuperclass(), target );
161            }
162        }
163
164
165        private CacheItem getCacheEntry( Class<?> cls )
166        {
167            CacheItem cacheItem = CACHED_ENTRIES.get( cls );
168            if ( cacheItem == null )
169            {
170                cacheItem = new CacheItem( cls );
171                CACHED_ENTRIES.put( cls, cacheItem );
172            }
173            return cacheItem;
174        }
175
176        private static void evaluateArray( Object target, InterpolateObjectAction ctx )
177        {
178            int len = Array.getLength( target );
179            for ( int i = 0; i < len; i++ )
180            {
181                Object value = Array.get( target, i );
182                if ( value != null )
183                {
184                    if ( String.class == value.getClass() )
185                    {
186                        String interpolated = ctx.interpolate( (String) value );
187
188                        if ( !interpolated.equals( value ) )
189                        {
190                            Array.set( target, i, interpolated );
191                        }
192                    }
193                    else
194                    {
195                        ctx.interpolationTargets.add( value );
196                    }
197                }
198            }
199        }
200
201        private static class CacheItem
202        {
203            private final boolean isArray;
204
205            private final boolean isQualifiedForInterpolation;
206
207            private final CacheField[] fields;
208
209            private boolean isQualifiedForInterpolation( Class<?> cls )
210            {
211                return !cls.getName().startsWith( "java" );
212            }
213
214            private boolean isQualifiedForInterpolation( Field field, Class<?> fieldType )
215            {
216                if ( Map.class.equals( fieldType ) && "locations".equals( field.getName() ) )
217                {
218                    return false;
219                }
220
221                //noinspection SimplifiableIfStatement
222                if ( fieldType.isPrimitive() )
223                {
224                    return false;
225                }
226
227                return !"parent".equals( field.getName() );
228            }
229
230            CacheItem( Class clazz )
231            {
232                this.isQualifiedForInterpolation = isQualifiedForInterpolation( clazz );
233                this.isArray = clazz.isArray();
234                List<CacheField> fields = new ArrayList<>();
235                for ( Field currentField : clazz.getDeclaredFields() )
236                {
237                    Class<?> type = currentField.getType();
238                    if ( isQualifiedForInterpolation( currentField, type ) )
239                    {
240                        if ( String.class == type )
241                        {
242                            if ( !Modifier.isFinal( currentField.getModifiers() ) )
243                            {
244                                fields.add( new StringField( currentField ) );
245                            }
246                        }
247                        else if ( List.class.isAssignableFrom( type ) )
248                        {
249                            fields.add( new ListField( currentField ) );
250                        }
251                        else if ( Collection.class.isAssignableFrom( type ) )
252                        {
253                            throw new RuntimeException( "We dont interpolate into collections, use a list instead" );
254                        }
255                        else if ( Map.class.isAssignableFrom( type ) )
256                        {
257                            fields.add( new MapField( currentField ) );
258                        }
259                        else
260                        {
261                            fields.add( new ObjectField( currentField ) );
262                        }
263                    }
264
265                }
266                this.fields = fields.toArray( new CacheField[fields.size()] );
267
268            }
269
270            public void interpolate( Object target, InterpolateObjectAction interpolateObjectAction )
271            {
272                for ( CacheField field : fields )
273                {
274                    field.interpolate( target, interpolateObjectAction );
275                }
276            }
277
278            public boolean isArray()
279            {
280                return isArray;
281            }
282        }
283
284        abstract static class CacheField
285        {
286            protected final Field field;
287
288            CacheField( Field field )
289            {
290                this.field = field;
291            }
292
293            void interpolate( Object target, InterpolateObjectAction interpolateObjectAction )
294            {
295                synchronized ( field )
296                {
297                    boolean isAccessible = field.isAccessible();
298                    field.setAccessible( true );
299                    try
300                    {
301                        doInterpolate( target, interpolateObjectAction );
302                    }
303                    catch ( IllegalArgumentException e )
304                    {
305                        interpolateObjectAction.problems.add(
306                            new ModelProblemCollectorRequest( Severity.ERROR, Version.BASE ).setMessage(
307                                "Failed to interpolate field3: " + field + " on class: "
308                                    + field.getType().getName() ).setException(
309                                e ) ); // todo: Not entirely the same message
310                    }
311                    catch ( IllegalAccessException e )
312                    {
313                        interpolateObjectAction.problems.add(
314                            new ModelProblemCollectorRequest( Severity.ERROR, Version.BASE ).setMessage(
315                                "Failed to interpolate field4: " + field + " on class: "
316                                    + field.getType().getName() ).setException( e ) );
317                    }
318                    finally
319                    {
320                        field.setAccessible( isAccessible );
321                    }
322                }
323
324
325            }
326
327            abstract void doInterpolate( Object target, InterpolateObjectAction ctx )
328                throws IllegalAccessException;
329        }
330
331        static final class StringField
332            extends CacheField
333        {
334            StringField( Field field )
335            {
336                super( field );
337            }
338
339            @Override
340            void doInterpolate( Object target, InterpolateObjectAction ctx )
341                throws IllegalAccessException
342            {
343                String value = (String) field.get( target );
344                if ( value == null )
345                {
346                    return;
347                }
348
349                String interpolated = ctx.interpolate( value );
350
351                if ( !interpolated.equals( value ) )
352                {
353                    field.set( target, interpolated );
354                }
355            }
356        }
357
358        static final class ListField
359            extends CacheField
360        {
361            ListField( Field field )
362            {
363                super( field );
364            }
365
366            @Override
367            void doInterpolate( Object target, InterpolateObjectAction ctx )
368                throws IllegalAccessException
369            {
370                @SuppressWarnings( "unchecked" ) List<Object> c = (List<Object>) field.get( target );
371                if ( c == null )
372                {
373                    return;
374                }
375
376                int size = c.size();
377                Object value;
378                for ( int i = 0; i < size; i++ )
379                {
380
381                    value = c.get( i );
382
383                    if ( value != null )
384                    {
385                        if ( String.class == value.getClass() )
386                        {
387                            String interpolated = ctx.interpolate( (String) value );
388
389                            if ( !interpolated.equals( value ) )
390                            {
391                                try
392                                {
393                                    c.set( i, interpolated );
394                                }
395                                catch ( UnsupportedOperationException e )
396                                {
397                                    return;
398                                }
399                            }
400                        }
401                        else
402                        {
403                            if ( value.getClass().isArray() )
404                            {
405                                evaluateArray( value, ctx );
406                            }
407                            else
408                            {
409                                ctx.interpolationTargets.add( value );
410                            }
411                        }
412                    }
413                }
414            }
415        }
416
417        static final class MapField
418            extends CacheField
419        {
420            MapField( Field field )
421            {
422                super( field );
423            }
424
425            @Override
426            void doInterpolate( Object target, InterpolateObjectAction ctx )
427                throws IllegalAccessException
428            {
429                @SuppressWarnings( "unchecked" ) Map<Object, Object> m = (Map<Object, Object>) field.get( target );
430                if ( m == null || m.isEmpty() )
431                {
432                    return;
433                }
434
435                for ( Map.Entry<Object, Object> entry : m.entrySet() )
436                {
437                    Object value = entry.getValue();
438
439                    if ( value == null )
440                    {
441                        continue;
442                    }
443
444                    if ( String.class == value.getClass() )
445                    {
446                        String interpolated = ctx.interpolate( (String) value );
447
448                        if ( !interpolated.equals( value ) )
449                        {
450                            try
451                            {
452                                entry.setValue( interpolated );
453                            }
454                            catch ( UnsupportedOperationException ignore )
455                            {
456                                // nop
457                            }
458                        }
459                    }
460                    else if ( value.getClass().isArray() )
461                    {
462                        evaluateArray( value, ctx );
463                    }
464                    else
465                    {
466                        ctx.interpolationTargets.add( value );
467                    }
468                }
469            }
470        }
471
472        static final class ObjectField
473            extends CacheField
474        {
475            private final boolean isArray;
476
477            ObjectField( Field field )
478            {
479                super( field );
480                this.isArray = field.getType().isArray();
481            }
482
483            @Override
484            void doInterpolate( Object target, InterpolateObjectAction ctx )
485                throws IllegalAccessException
486            {
487                Object value = field.get( target );
488                if ( value != null )
489                {
490                    if ( isArray )
491                    {
492                        evaluateArray( value, ctx );
493                    }
494                    else
495                    {
496                        ctx.interpolationTargets.add( value );
497                    }
498                }
499            }
500        }
501
502    }
503
504}