001/*
002 * Copyright (C) 2012 Facebook, Inc.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License"); you may
005 * not use this file except in compliance with the License. You may obtain
006 * a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013 * License for the specific language governing permissions and limitations
014 * under the License.
015 */
016package com.facebook.swift.codec.metadata;
017
018import com.facebook.swift.codec.ThriftField;
019import com.facebook.swift.codec.ThriftUnion;
020import com.facebook.swift.codec.ThriftUnionId;
021import com.facebook.swift.codec.metadata.ThriftStructMetadata.MetadataType;
022import com.google.common.base.Optional;
023import com.google.common.collect.ImmutableList;
024
025import javax.annotation.concurrent.NotThreadSafe;
026
027import java.lang.reflect.Field;
028import java.lang.reflect.Method;
029import java.lang.reflect.Modifier;
030import java.lang.reflect.Type;
031import java.util.Collection;
032import java.util.List;
033
034import static com.facebook.swift.codec.ThriftField.Requiredness;
035import static com.facebook.swift.codec.metadata.FieldKind.THRIFT_UNION_ID;
036import static com.facebook.swift.codec.metadata.ReflectionHelper.findAnnotatedMethods;
037
038@NotThreadSafe
039public class ThriftUnionMetadataBuilder
040    extends AbstractThriftMetadataBuilder
041{
042    public ThriftUnionMetadataBuilder(ThriftCatalog catalog, Type structType)
043    {
044        super(catalog, structType);
045
046        // verify the class is public and has the correct annotations
047        verifyClass(ThriftUnion.class);
048
049        // extract the @ThriftUnionId fields
050        extractThriftUnionId();
051
052        // finally normalize the field metadata using things like
053        normalizeThriftFields(catalog);
054    }
055
056    @Override
057    protected String extractName()
058    {
059        ThriftUnion annotation = getStructClass().getAnnotation(ThriftUnion.class);
060        if (annotation == null) {
061            return getStructClass().getSimpleName();
062        }
063        else if (!annotation.value().isEmpty()) {
064            return annotation.value();
065        }
066        else {
067            return getStructClass().getSimpleName();
068        }
069    }
070
071    @Override
072    protected Class<?> extractBuilderClass()
073    {
074        ThriftUnion annotation = getStructClass().getAnnotation(ThriftUnion.class);
075        if (annotation != null && !annotation.builder().equals(void.class)) {
076            return annotation.builder();
077        }
078        else {
079            return null;
080        }
081    }
082
083    private void extractThriftUnionId()
084    {
085        Collection<Field> idFields = ReflectionHelper.findAnnotatedFields(getStructClass(), ThriftUnionId.class);
086        Collection<Method> idMethods = findAnnotatedMethods(getStructClass(), ThriftUnionId.class);
087
088        if (idFields.size() + idMethods.size() != 1) {
089            if (idFields.size() + idMethods.size() == 0) {
090                metadataErrors.addError("Neither a field nor a method is annotated with @ThriftUnionId");
091            }
092            else  if (idFields.size() > 1) {
093                metadataErrors.addError("More than one @ThriftUnionId field present");
094            }
095            else if (idMethods.size() > 1) {
096                metadataErrors.addError("More than one @ThriftUnionId method present");
097            }
098            else {
099                metadataErrors.addError("Both fields and methods annotated with @ThriftUnionId");
100            }
101            return;
102        }
103
104        for (Field idField : idFields) {
105            FieldExtractor fieldExtractor = new FieldExtractor(structType, idField, null, THRIFT_UNION_ID);
106            fields.add(fieldExtractor);
107            extractors.add(fieldExtractor);
108
109            FieldInjection fieldInjection = new FieldInjection(structType, idField, null, THRIFT_UNION_ID);
110            fields.add(fieldInjection);
111            fieldInjections.add(fieldInjection);
112        }
113
114        for (Method idMethod: idMethods) {
115            if (!Modifier.isPublic(idMethod.getModifiers())) {
116                metadataErrors.addError("@ThriftUnionId method [%s] is not public", idMethod.toGenericString());
117                continue;
118            }
119            if (Modifier.isStatic(idMethod.getModifiers())) {
120                metadataErrors.addError("@ThriftUnionId method [%s] is static", idMethod.toGenericString());
121                continue;
122            }
123
124            if (isValidateGetter(idMethod)) {
125                MethodExtractor methodExtractor = new MethodExtractor(structType, idMethod, null, THRIFT_UNION_ID);
126                fields.add(methodExtractor);
127                extractors.add(methodExtractor);
128            }
129        }
130    }
131
132    @Override
133    protected void validateConstructors()
134    {
135        for (ConstructorInjection constructorInjection : constructorInjections) {
136            if (constructorInjection.getParameters().size() > 1) {
137                metadataErrors.addError("@ThriftConstructor [%s] takes %d arguments, this is illegal for an union",
138                                        constructorInjection.getConstructor().toGenericString(),
139                                        constructorInjection.getParameters().size());
140            }
141        }
142    }
143
144    @Override
145    protected boolean isValidateSetter(Method method)
146    {
147        // Unions only allow setters with exactly one parameters
148        return method.getParameterTypes().length == 1;
149    }
150
151    //
152    // Build final metadata
153    //
154    @Override
155    public ThriftStructMetadata build()
156    {
157        // this code assumes that metadata is clean
158        metadataErrors.throwIfHasErrors();
159
160        // builder constructor injection
161        ThriftMethodInjection builderMethodInjection = buildBuilderConstructorInjections();
162
163        // constructor injection (or factory method for builder)
164        ThriftConstructorInjection constructorInjection = buildConstructorInjection();
165
166        // fields injections
167        Iterable<ThriftFieldMetadata> fieldsMetadata = buildFieldInjections();
168
169        // methods injections
170        List<ThriftMethodInjection> methodInjections = buildMethodInjections();
171
172        return new ThriftStructMetadata(
173                structName,
174                structType,
175                builderType,
176                MetadataType.UNION,
177                Optional.fromNullable(builderMethodInjection),
178                ImmutableList.copyOf(documentation),
179                ImmutableList.copyOf(fieldsMetadata),
180                Optional.fromNullable(constructorInjection),
181                methodInjections
182        );
183    }
184
185    private ThriftConstructorInjection buildConstructorInjection()
186    {
187        for (ConstructorInjection constructorInjection : constructorInjections) {
188            if (constructorInjection.getParameters().size() == 0) {
189                return new ThriftConstructorInjection(constructorInjection.getConstructor(), buildParameterInjections(constructorInjection.getParameters()));
190            }
191        }
192
193        // This is actually legal for a ThriftUnion, all c'tors available take arguments and are associated with the FieldMetadata...
194        return null;
195    }
196
197    @Override
198    protected ThriftFieldMetadata buildField(Collection<FieldMetadata> input)
199    {
200        short id = -1;
201        String name = null;
202        Requiredness requiredness = Requiredness.UNSPECIFIED;
203        FieldKind fieldType = FieldKind.THRIFT_FIELD;
204        ThriftType thriftType = null;
205        ThriftConstructorInjection thriftConstructorInjection = null;
206        ThriftMethodInjection thriftMethodInjection = null;
207
208        // process field injections and extractions
209        ImmutableList.Builder<ThriftInjection> injections = ImmutableList.builder();
210        ThriftExtraction extraction = null;
211        for (FieldMetadata fieldMetadata : input) {
212            id = fieldMetadata.getId();
213            name = fieldMetadata.getName();
214            requiredness = fieldMetadata.getRequiredness();
215            fieldType = fieldMetadata.getType();
216            thriftType = catalog.getThriftType(fieldMetadata.getJavaType());
217
218            switch (requiredness) {
219                case REQUIRED:
220                case OPTIONAL:
221                    metadataErrors.addError(
222                            "Thrift union '%s' field '%s(%s)' should not be marked required or optional",
223                            structName,
224                            name,
225                            id);
226                    break;
227
228                default:
229                    break;
230            }
231
232            if (fieldMetadata instanceof FieldInjection) {
233                FieldInjection fieldInjection = (FieldInjection) fieldMetadata;
234                injections.add(new ThriftFieldInjection(fieldInjection.getId(), fieldInjection.getName(), fieldInjection.getField(), fieldInjection.getType()));
235            }
236            else if (fieldMetadata instanceof ParameterInjection) {
237                ParameterInjection parameterInjection = (ParameterInjection) fieldMetadata;
238                ThriftParameterInjection thriftParameterInjection =  new ThriftParameterInjection(
239                        parameterInjection.getId(),
240                        parameterInjection.getName(),
241                        parameterInjection.getParameterIndex(),
242                        fieldMetadata.getJavaType()
243                );
244                injections.add(thriftParameterInjection);
245
246                for (ConstructorInjection constructorInjection : constructorInjections) {
247                    if (constructorInjection.getParameters().size() == 1 && constructorInjection.getParameters().get(0).equals(parameterInjection)) {
248                        thriftConstructorInjection = new ThriftConstructorInjection(constructorInjection.getConstructor(), thriftParameterInjection);
249                        break;
250                    }
251                }
252
253                for (MethodInjection methodInjection : methodInjections) {
254                    if (methodInjection.getParameters().size() == 1 && methodInjection.getParameters().get(0).equals(parameterInjection)) {
255                        thriftMethodInjection = new ThriftMethodInjection(methodInjection.getMethod(), thriftParameterInjection);
256                    }
257                }
258            }
259            else if (fieldMetadata instanceof FieldExtractor) {
260                FieldExtractor fieldExtractor = (FieldExtractor) fieldMetadata;
261                extraction = new ThriftFieldExtractor(fieldExtractor.getId(), fieldExtractor.getName(), fieldExtractor.getType(), fieldExtractor.getField(), fieldExtractor.getJavaType());
262            }
263            else if (fieldMetadata instanceof MethodExtractor) {
264                MethodExtractor methodExtractor = (MethodExtractor) fieldMetadata;
265                extraction = new ThriftMethodExtractor(methodExtractor.getId(), methodExtractor.getName(), methodExtractor.getType(), methodExtractor.getMethod(), methodExtractor.getJavaType());
266            }
267        }
268
269        // add type coercion
270        TypeCoercion coercion = null;
271        if (thriftType.isCoerced()) {
272            coercion = catalog.getDefaultCoercion(thriftType.getJavaType());
273        }
274
275        ThriftFieldMetadata thriftFieldMetadata = new ThriftFieldMetadata(
276                id,
277                requiredness,
278                thriftType,
279                name,
280                fieldType,
281                injections.build(),
282                Optional.fromNullable(thriftConstructorInjection),
283                Optional.fromNullable(thriftMethodInjection),
284                Optional.fromNullable(extraction),
285                Optional.fromNullable(coercion)
286        );
287        return thriftFieldMetadata;
288    }
289}