001package ball.annotation.processing;
002/*-
003 * ##########################################################################
004 * Utilities
005 * %%
006 * Copyright (C) 2008 - 2022 Allen D. Ball
007 * %%
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * 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, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 * ##########################################################################
020 */
021import ball.annotation.ServiceProviderFor;
022import java.io.IOException;
023import java.io.InputStream;
024import java.io.OutputStream;
025import java.lang.annotation.Annotation;
026import java.lang.reflect.Method;
027import java.net.URI;
028import java.util.Set;
029import java.util.jar.Attributes;
030import java.util.jar.Manifest;
031import javax.annotation.processing.Processor;
032import javax.annotation.processing.RoundEnvironment;
033import javax.lang.model.element.Element;
034import javax.lang.model.element.ExecutableElement;
035import javax.lang.model.element.TypeElement;
036import javax.tools.FileObject;
037import javax.tools.JavaFileManager;
038import lombok.NoArgsConstructor;
039import lombok.ToString;
040
041import static ball.annotation.Manifest.Attribute;
042import static ball.annotation.Manifest.DependsOn;
043import static ball.annotation.Manifest.DesignTimeOnly;
044import static ball.annotation.Manifest.JavaBean;
045import static ball.annotation.Manifest.MainClass;
046import static ball.annotation.Manifest.Section;
047import static javax.tools.Diagnostic.Kind.ERROR;
048import static javax.tools.JavaFileObject.Kind.CLASS;
049import static javax.tools.StandardLocation.CLASS_OUTPUT;
050import static javax.tools.StandardLocation.CLASS_PATH;
051import static org.apache.commons.lang3.StringUtils.EMPTY;
052
053/**
054 * {@link Processor} implementation to scan {@link ball.annotation.Manifest}
055 * {@link java.lang.annotation.Annotation}s and generate a
056 * {@link java.util.jar.Manifest META-INF/MANIFEST.MF}.
057 *
058 * @author {@link.uri mailto:ball@hcf.dev Allen D. Ball}
059 */
060@ServiceProviderFor({ Processor.class })
061@For({ Attribute.class, MainClass.class, Section.class, JavaBean.class, DependsOn.class, DesignTimeOnly.class })
062@NoArgsConstructor @ToString
063public class ManifestProcessor extends AnnotatedProcessor implements ClassFileProcessor {
064    private static final String PATH = "META-INF/MANIFEST.MF";
065
066    private static abstract class PROTOTYPE {
067        public static void main(String[] argv) { }
068    }
069
070    private static final Method PROTOTYPE = PROTOTYPE.class.getDeclaredMethods()[0];
071
072    static { PROTOTYPE.setAccessible(true); }
073
074    @Override
075    protected void process(RoundEnvironment roundEnv, TypeElement annotation, Element element) {
076        super.process(roundEnv, annotation, element);
077
078        Attribute attribute = element.getAnnotation(Attribute.class);
079        MainClass main = element.getAnnotation(MainClass.class);
080
081        if (main != null) {
082            switch (element.getKind()) {
083            case CLASS:
084            case INTERFACE:
085                TypeElement type = (TypeElement) element;
086                ExecutableElement method = getMethod(type, PROTOTYPE);
087
088                if (method != null && (method.getModifiers().containsAll(getModifiers(PROTOTYPE)))) {
089                } else {
090                    print(ERROR, element,
091                          "@%s: %s does not implement '%s'",
092                          main.annotationType().getSimpleName(), element.getKind(), declaration(PROTOTYPE));
093                }
094                break;
095
096            default:
097                break;
098            }
099        }
100
101        Section section = element.getAnnotation(Section.class);
102        JavaBean bean = element.getAnnotation(JavaBean.class);
103        DependsOn depends = element.getAnnotation(DependsOn.class);
104        DesignTimeOnly design = element.getAnnotation(DesignTimeOnly.class);
105    }
106
107    @Override
108    public void process(Set<Class<?>> set, JavaFileManager fm) throws Exception {
109        ManifestImpl manifest = new ManifestImpl();
110        FileObject file = fm.getFileForInput(CLASS_PATH, EMPTY, PATH);
111
112        if (file != null) {
113            try (InputStream in = file.openInputStream()) {
114                manifest.read(in);
115            } catch (IOException exception) {
116            }
117        } else {
118            manifest.init();
119        }
120
121        file = fm.getFileForOutput(CLASS_OUTPUT, EMPTY, PATH, null);
122
123        URI root = file.toUri().resolve("..").normalize();
124
125        for (Class<?> type : set) {
126            URI uri = fm.getJavaFileForInput(CLASS_OUTPUT, type.getName(), CLASS).toUri();
127
128            uri = root.relativize(uri);
129
130            Attribute attribute = type.getAnnotation(Attribute.class);
131            MainClass main = type.getAnnotation(MainClass.class);
132
133            if (main != null) {
134                manifest.put(main, type.getName());
135            }
136
137            Section section = type.getAnnotation(Section.class);
138
139            if (section != null) {
140                manifest.put(uri.resolve("").toString(), section);
141            }
142
143            JavaBean bean = type.getAnnotation(JavaBean.class);
144            DependsOn depends = type.getAnnotation(DependsOn.class);
145            DesignTimeOnly design = type.getAnnotation(DesignTimeOnly.class);
146
147            if (bean != null || depends != null || design != null) {
148                manifest.put(uri.toString(), bean, depends, design);
149            }
150        }
151
152        try (OutputStream out = file.openOutputStream()) {
153            manifest.write(out);
154         }
155    }
156
157    @NoArgsConstructor @ToString
158    private class ManifestImpl extends Manifest {
159        private static final String MANIFEST_VERSION = "Manifest-Version";
160
161        protected void init() {
162            if (getMainAttributes().getValue(MANIFEST_VERSION) == null) {
163                getMainAttributes().putValue(MANIFEST_VERSION, "1.0");
164            }
165        }
166
167        public Attributes putAttributes(String name, Attributes attributes) {
168            return getEntries().put(name, attributes);
169        }
170
171        public String put(MainClass main, String name) {
172            return getMainAttributes().putValue(getAttributeName(main.getClass()), name);
173        }
174
175        public void put(String path, Section section) {
176            Attributes attributes = getEntries().get(path);
177
178            if (attributes == null) {
179                attributes = new Attributes();
180                getEntries().put(path, attributes);
181            }
182
183            attributes.putValue("Sealed", String.valueOf(section.sealed()));
184        }
185
186        public void put(String path, JavaBean bean, DependsOn depends, DesignTimeOnly design) {
187            Attributes attributes = getEntries().get(path);
188
189            if (attributes == null) {
190                attributes = new Attributes();
191                getEntries().put(path, attributes);
192            }
193
194            attributes.putValue(getAttributeName(JavaBean.class), String.valueOf(true));
195
196            if (depends != null) {
197                attributes.putValue(getAttributeName(depends.getClass()), String.join(" ", depends.value()));
198            }
199
200            if (design != null) {
201                attributes.putValue(getAttributeName(design.getClass()), String.join(" ", design.value()));
202            }
203        }
204
205        private String getAttributeName(Class<? extends Annotation> type) {
206            return type.getAnnotation(Attribute.class).value();
207        }
208    }
209}