View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugins.shade.resource.rule;
20  
21  import java.io.ByteArrayInputStream;
22  import java.io.ByteArrayOutputStream;
23  import java.io.IOException;
24  import java.lang.annotation.Retention;
25  import java.lang.annotation.Target;
26  import java.nio.charset.StandardCharsets;
27  import java.util.Collections;
28  import java.util.HashMap;
29  import java.util.Map;
30  import java.util.jar.JarEntry;
31  import java.util.jar.JarInputStream;
32  import java.util.jar.JarOutputStream;
33  
34  import org.apache.maven.plugins.shade.relocation.Relocator;
35  import org.apache.maven.plugins.shade.resource.ReproducibleResourceTransformer;
36  import org.codehaus.plexus.component.configurator.ComponentConfigurationException;
37  import org.codehaus.plexus.component.configurator.converters.ConfigurationConverter;
38  import org.codehaus.plexus.component.configurator.converters.lookup.ConverterLookup;
39  import org.codehaus.plexus.component.configurator.converters.lookup.DefaultConverterLookup;
40  import org.codehaus.plexus.component.configurator.expression.DefaultExpressionEvaluator;
41  import org.codehaus.plexus.configuration.DefaultPlexusConfiguration;
42  import org.codehaus.plexus.configuration.PlexusConfiguration;
43  import org.junit.rules.TestRule;
44  import org.junit.runner.Description;
45  import org.junit.runners.model.Statement;
46  
47  import static java.lang.annotation.ElementType.METHOD;
48  import static java.lang.annotation.RetentionPolicy.RUNTIME;
49  import static org.junit.Assert.assertNotNull;
50  import static org.junit.Assert.assertTrue;
51  import static org.junit.Assert.fail;
52  
53  public class TransformerTesterRule implements TestRule {
54      @Override
55      public Statement apply(final Statement base, final Description description) {
56          return new Statement() {
57              @Override
58              public void evaluate() throws Throwable {
59                  final TransformerTest spec = description.getAnnotation(TransformerTest.class);
60                  if (spec == null) {
61                      base.evaluate();
62                      return;
63                  }
64  
65                  final Map<String, String> jar;
66                  try {
67                      final ReproducibleResourceTransformer transformer = createTransformer(spec);
68                      visit(spec, transformer);
69                      jar = captureOutput(transformer);
70                  } catch (final Exception ex) {
71                      if (Exception.class.isAssignableFrom(spec.expectedException())) {
72                          assertTrue(
73                                  ex.getClass().getName(),
74                                  spec.expectedException().isAssignableFrom(ex.getClass()));
75                          return;
76                      } else {
77                          throw ex;
78                      }
79                  }
80                  asserts(spec, jar);
81              }
82          };
83      }
84  
85      private void asserts(final TransformerTest spec, final Map<String, String> jar) {
86          if (spec.strictMatch() && jar.size() != spec.expected().length) {
87              fail("Strict match test failed: " + jar);
88          }
89          for (final Resource expected : spec.expected()) {
90              final String content = jar.get(expected.path());
91              assertNotNull(expected.path(), content);
92              assertTrue(
93                      expected.path() + ", expected=" + expected.content() + ", actual=" + content,
94                      content.replace(System.lineSeparator(), "\n").matches(expected.content()));
95          }
96      }
97  
98      private Map<String, String> captureOutput(final ReproducibleResourceTransformer transformer) throws IOException {
99          final ByteArrayOutputStream out = new ByteArrayOutputStream();
100         try (final JarOutputStream jar = new JarOutputStream(out)) {
101             transformer.modifyOutputStream(jar);
102         }
103 
104         final Map<String, String> created = new HashMap<>();
105         try (final JarInputStream jar = new JarInputStream(new ByteArrayInputStream(out.toByteArray()))) {
106             JarEntry entry;
107             while ((entry = jar.getNextJarEntry()) != null) {
108                 created.put(entry.getName(), read(jar));
109             }
110         }
111         return created;
112     }
113 
114     private void visit(final TransformerTest spec, final ReproducibleResourceTransformer transformer)
115             throws IOException {
116         for (final Resource resource : spec.visited()) {
117             if (transformer.canTransformResource(resource.path())) {
118                 transformer.processResource(
119                         resource.path(),
120                         new ByteArrayInputStream(resource.content().getBytes(StandardCharsets.UTF_8)),
121                         Collections.<Relocator>emptyList(),
122                         0);
123             }
124         }
125     }
126 
127     private String read(final JarInputStream jar) throws IOException {
128         final StringBuilder builder = new StringBuilder();
129         final byte[] buffer = new byte[512];
130         int read;
131         while ((read = jar.read(buffer)) >= 0) {
132             builder.append(new String(buffer, 0, read));
133         }
134         return builder.toString();
135     }
136 
137     private ReproducibleResourceTransformer createTransformer(final TransformerTest spec) {
138         final ConverterLookup lookup = new DefaultConverterLookup();
139         try {
140             final ConfigurationConverter converter = lookup.lookupConverterForType(spec.transformer());
141             final PlexusConfiguration configuration = new DefaultPlexusConfiguration("configuration");
142             for (final Property property : spec.configuration()) {
143                 configuration.addChild(property.name(), property.value());
144             }
145             return ReproducibleResourceTransformer.class.cast(converter.fromConfiguration(
146                     lookup,
147                     configuration,
148                     spec.transformer(),
149                     spec.transformer(),
150                     Thread.currentThread().getContextClassLoader(),
151                     new DefaultExpressionEvaluator()));
152         } catch (final ComponentConfigurationException e) {
153             throw new IllegalStateException(e);
154         }
155     }
156 
157     /**
158      * Enables to describe a test without having to implement the logic itself.
159      */
160     @Target(METHOD)
161     @Retention(RUNTIME)
162     public @interface TransformerTest {
163         /**
164          * @return the list of resource the transformer will process.
165          */
166         Resource[] visited();
167 
168         /**
169          * @return the expected output created by the transformer.
170          */
171         Resource[] expected();
172 
173         /**
174          * @return true if only expected resources must be found.
175          */
176         boolean strictMatch() default true;
177 
178         /**
179          * @return type of transformer to use.
180          */
181         Class<?> transformer();
182 
183         /**
184          * @return transformer configuration.
185          */
186         Property[] configuration();
187 
188         /**
189          * @return if set to an exception class it ensures it is thrown during the processing.
190          */
191         Class<?> expectedException() default Object.class;
192     }
193 
194     @Target(METHOD)
195     @Retention(RUNTIME)
196     public @interface Property {
197         String name();
198 
199         String value();
200     }
201 
202     @Target(METHOD)
203     @Retention(RUNTIME)
204     public @interface Resource {
205         String path();
206 
207         String content();
208     }
209 }