All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.getperka.flatpack.fast.ObjcDialect Maven / Gradle / Ivy

There is a newer version: 2.21.0
Show newest version
package com.getperka.flatpack.fast;

/*
 * #%L
 * FlatPack Automatic Source Tool
 * %%
 * Copyright (C) 2012 - 2013 Perka Inc.
 * %%
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *      http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 * #L%
 */

import static org.jvnet.inflector.Noun.pluralOf;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.UnsupportedEncodingException;
import java.io.Writer;
import java.net.URLDecoder;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.stringtemplate.v4.AttributeRenderer;
import org.stringtemplate.v4.AutoIndentWriter;
import org.stringtemplate.v4.Interpreter;
import org.stringtemplate.v4.ST;
import org.stringtemplate.v4.STGroup;
import org.stringtemplate.v4.STGroupFile;
import org.stringtemplate.v4.misc.ObjectModelAdaptor;
import org.stringtemplate.v4.misc.STNoSuchPropertyException;

import com.getperka.cli.flags.Flag;
import com.getperka.flatpack.BaseHasUuid;
import com.getperka.flatpack.client.dto.ApiDescription;
import com.getperka.flatpack.client.dto.EndpointDescription;
import com.getperka.flatpack.client.dto.EntityDescription;
import com.getperka.flatpack.client.dto.ParameterDescription;
import com.getperka.flatpack.ext.JsonKind;
import com.getperka.flatpack.ext.Property;
import com.getperka.flatpack.ext.Type;
import com.getperka.flatpack.util.FlatPackCollections;

public class ObjcDialect implements Dialect {

  @Flag(tag = "classPrefix",
      help = "The prefix to add to all class names",
      defaultValue = "FP")
  static String classPrefix;

  private static final List KEYWORDS = Arrays.asList(
      "void",
      "char",
      "short",
      "int",
      "long",
      "float",
      "double",
      "signed",
      "unsigned",
      "id",
      "const",
      "volatile",
      "in",
      "out",
      "inout",
      "bycopy",
      "byref",
      "oneway",
      "self",
      "super");

  private static final Logger logger = LoggerFactory.getLogger(ObjcDialect.class);

  private static String downcase(String s) {
    return Character.toLowerCase(s.charAt(0)) + s.substring(1);
  }

  private static String upcase(String s) {
    return Character.toUpperCase(s.charAt(0)) + s.substring(1);
  }

  @Override
  public void generate(ApiDescription api, File outputDir) throws IOException {
    if (!outputDir.isDirectory() && !outputDir.mkdirs()) {
      logger.error("Could not create output directory {}", outputDir.getPath());
      return;
    }

    // first collect just our model entities
    Map allEntities = FlatPackCollections
        .mapForIteration();
    for (EntityDescription entity : api.getEntities()) {
      allEntities.put(entity.getTypeName(), entity);
      for (Iterator it = entity.getProperties().iterator(); it.hasNext();) {
        Property prop = it.next();
        // Remove the uuid property
        if ("uuid".equals(prop.getName())) {
          it.remove();
        }

        // and properties not declared in the current type
        else if (!prop.getEnclosingTypeName().equals(entity.getTypeName())) {
          it.remove();
        }
      }
    }
    // Ensure that the "real" implementations are used
    allEntities.remove("baseHasUuid");
    allEntities.remove("hasUuid");

    // Render entities
    STGroup group = loadGroup();
    ST entityST = null;
    for (EntityDescription entity : allEntities.values()) {
      entityST = group.getInstanceOf("entityHeader").add("entity", entity);
      render(entityST, outputDir, upcase(entity.getTypeName()) + ".h");

      entityST = group.getInstanceOf("entity").add("entity", entity);
      render(entityST, outputDir, upcase(entity.getTypeName()) + ".m");
    }

    // render api stubs
    ST apiHeaderST = group.getInstanceOf("apiHeader").add("api", api);
    render(apiHeaderST, outputDir, "BaseApi.h");
    ST apiST = group.getInstanceOf("api").add("api", api);
    render(apiST, outputDir, "BaseApi.m");
  }

  @Override
  public String getDialectName() {
    return "objc";
  }

  /**
   * Converts the given docString to be doxygen compatible
   * 
   * @param docString
   * @return
   */
  private String doxygenDocString(String docString) {
    if (docString == null) return docString;

    String newDocString = docString;
    try {
      // replace  tags with /link /endlink pairs
      Pattern regex = Pattern.compile(
          "([^<]*)",
          Pattern.CASE_INSENSITIVE);
      Matcher matcher = regex.matcher(docString);
      while (matcher.find()) {
        Type type = new Type.Builder().withName(matcher.group(1).trim()).build();
        String objcType = objcTypeForType(type);

        String link = "\\\\link " + objcType + " " +
          matcher.group(2).trim() + " \\\\endlink";
        newDocString = newDocString.replaceAll(matcher.group(0), link);
      }

      // replace #getFoo() method calls to #foo() method calls
      regex = Pattern.compile(
          "#get([^(]*)" + Pattern.quote("()"),
          Pattern.CASE_INSENSITIVE);
      matcher = regex.matcher(docString);
      while (matcher.find()) {
        String newMethod = "#" + downcase(matcher.group(1));
        newDocString = newDocString.replaceAll(matcher.group(0), newMethod);
      }
    }

    catch (Exception e) {
      logger.error("Couldn't doxygenize doc string: " + docString, e);
    }
    return newDocString;
  }

  private String getBuilderReturnType(EndpointDescription end) {
    // if the endpoint has no query parameters, we can simply use the FPFlatpackRequest
    // if (end.getQueryParameters() == null || end.getQueryParameters().size() == 0) {
    // return "FPFlatpackRequest";
    // }

    // Convert a path like /api/2/foo/bar/{}/baz to FooBarBazMethod
    String path = end.getPath();
    String[] parts = path.split(Pattern.quote("/"));
    StringBuilder sb = new StringBuilder();
    sb.append(classPrefix);
    sb.append(upcase(end.getMethod().toLowerCase()));
    for (int i = 3, j = parts.length; i < j; i++) {
      try {
        String part = parts[i];
        if (part.length() == 0) {
          continue;
        }
        StringBuilder decodedPart = new StringBuilder(URLDecoder
            .decode(part, "UTF8"));
        // Trim characters that aren't legal
        for (int k = decodedPart.length() - 1; k >= 0; k--) {
          if (!Character.isJavaIdentifierPart(decodedPart.charAt(k))) {
            decodedPart.deleteCharAt(k);
          }
        }
        // Append the new name part, using camel-cased names
        String newPart = decodedPart.toString();
        if (sb.length() > 0) {
          newPart = upcase(newPart);
        }
        sb.append(newPart);
      } catch (UnsupportedEncodingException e) {
        throw new RuntimeException(e);
      }
    }
    sb.append("Request");

    return upcase(sb.toString());
  }

  private String getMethodizedPath(EndpointDescription end) {
    String path = end.getPath();
    String[] parts = path.split(Pattern.quote("/"));
    StringBuilder sb = new StringBuilder();
    sb.append(end.getMethod().toLowerCase());
    int paramCount = 0;
    for (int i = 3, j = parts.length; i < j; i++) {
      String part = parts[i];
      if (part.length() == 0) continue;

      if (part.startsWith("{") && part.endsWith("}")) {
        String name = part.substring(1, part.length() - 1);
        sb.append(paramCount > 0 ? name : upcase(name));
        sb.append(":(NSString *)" + name);
        if (i < parts.length - 1) {
          sb.append(" ");
        }
        paramCount++;
      }

      else {
        sb.append(upcase(part));
      }
    }

    return sb.toString();
  }

  private String getSafeName(String name) {
    return name + (KEYWORDS.contains(name) ? "Property" : "");
  }

  private boolean isRequiredImport(String type) {
    return type != null && !type.equalsIgnoreCase("nil") && !type.startsWith("NS");
  }

  /**
   * Load {@code objc.stg} from the classpath and configure a number of model adaptors to add
   * virtual properties to the objects being rendered.
   */
  private STGroup loadGroup() {

    STGroup group = new STGroupFile(getClass().getResource("objc.stg"), "UTF8", '<', '>');
    // EntityDescription are rendered as the FQN
    group.registerRenderer(EntityDescription.class, new AttributeRenderer() {

      @Override
      public String toString(Object o, String formatString, Locale locale) {
        EntityDescription entity = (EntityDescription) o;
        if (entity.getTypeName().equals("baseHasUuid")) {
          return BaseHasUuid.class.getCanonicalName();
        }
        return entity.getTypeName();
      }
    });

    group.registerModelAdaptor(ApiDescription.class,
        new ObjectModelAdaptor() {
          @Override
          public Object getProperty(Interpreter interp, ST self, Object o,
              Object property, String propertyName)
              throws STNoSuchPropertyException {
            ApiDescription apiDescription = (ApiDescription) o;
            if ("endpoints".equals(propertyName)) {
              List sortedEndpoints = new ArrayList(
                  apiDescription.getEndpoints());
              Collections.sort(sortedEndpoints, new Comparator() {
                @Override
                public int compare(EndpointDescription e1, EndpointDescription e2) {
                  return e1.getPath().compareTo(e2.getPath());
                }
              });
              return sortedEndpoints;
            }
            else if ("importNames".equals(propertyName)) {
              Set imports = new HashSet();
              for (EndpointDescription e : apiDescription.getEndpoints()) {
                if (e.getEntity() != null) {
                  String type = objcTypeForType(e.getEntity());
                  if (isRequiredImport(type)) {
                    imports.add(type);
                  }
                }
                if (e.getReturnType() != null) {
                  String type = objcTypeForType(e.getReturnType());
                  if (isRequiredImport(type)) {
                    imports.add(type);
                  }
                }
              }
              List sortedImports = new ArrayList(imports);
              Collections.sort(sortedImports);
              return sortedImports;
            }
            return super.getProperty(interp, self, o, property, propertyName);
          }
        });

    group.registerModelAdaptor(EndpointDescription.class,
        new ObjectModelAdaptor() {
          @Override
          public Object getProperty(Interpreter interp, ST self, Object o,
              Object property, String propertyName)
              throws STNoSuchPropertyException {
            EndpointDescription end = (EndpointDescription) o;
            if ("docString".equals(propertyName)) {
              return doxygenDocString(end.getDocString());
            }
            else if ("methodName".equals(propertyName)) {
              StringBuilder sb = new StringBuilder();

              sb.append("- (" + getBuilderReturnType(end) + " *)");

              sb.append(getMethodizedPath(end));

              if (end.getEntity() != null) {
                if (end.getPathParameters() != null && end.getPathParameters().size() > 0) {
                  sb.append(" entity");
                }
                String type = objcTypeForType(end.getEntity());
                sb.append(":(" + type + " *)");
                String paramName = type;
                if (type.startsWith(classPrefix)) {
                  paramName = downcase(type.substring(classPrefix.length()));
                }
                sb.append(paramName);
              }

              return sb.toString();
            }

            else if ("requestBuilderClassName".equals(propertyName)) {
              return getBuilderReturnType(end);
            }

            else if ("requestBuilderBlockName".equals(propertyName)) {
              return getBuilderReturnType(end) + "Block";
            }

            else if ("entityReturnType".equals(propertyName)) {
              String type = objcFlatpackReturnType(end.getReturnType());
              return type.equals("void") ? null : type;
            }

            else if ("entityReturnName".equals(propertyName)) {
              if (end.getReturnType() == null) return null;

              String name = "result";
              if (end.getReturnType().getName() != null) {
                name = end.getReturnType().getName();
              }

              if (end.getReturnType().getListElement() != null) {
                if (end.getReturnType().getListElement().getName() != null) {
                  name = end.getReturnType().getListElement().getName();
                }
                name = pluralOf(name);
              }

              return name;
            }

            else if ("pathDecoded".equals(propertyName)) {
              // URL-decode the path in the endpoint description
              try {
                String decoded = URLDecoder.decode(end.getPath(), "UTF8");
                return decoded;
              } catch (UnsupportedEncodingException e) {
                throw new RuntimeException(e);
              }
            }
            return super.getProperty(interp, self, o, property, propertyName);
          }
        });

    group.registerModelAdaptor(EntityDescription.class,
        new ObjectModelAdaptor() {
          @Override
          public Object getProperty(Interpreter interp, ST self, Object o,
              Object property, String propertyName)
              throws STNoSuchPropertyException {

            EntityDescription entity = (EntityDescription) o;
            if ("importNames".equals(propertyName)) {
              Set imports = new HashSet();
              String type = requireNameForType(entity.getTypeName());
              if (isRequiredImport(type)) {
                imports.add(type);
              }
              for (Property p : entity.getProperties()) {
                String name = null;
                if (p.getType().getListElement() != null) {
                  name = objcTypeForType(p.getType().getListElement());
                }
                else {
                  name = objcTypeForProperty(p);
                }
                if (name != null && isRequiredImport(name)) {
                  imports.add(name);
                }
              }
              List sortedImports = new ArrayList(imports);
              Collections.sort(sortedImports);
              return sortedImports;
            }
            if ("docString".equals(propertyName)) {
              return doxygenDocString(entity.getDocString());
            }
            else if ("payloadName".equals(propertyName)) {
              return entity.getTypeName();
            }

            else if ("supertype".equals(propertyName)) {
              EntityDescription supertype = entity.getSupertype();
              return supertype == null ? new EntityDescription("baseHasUuid", null) : supertype;
            }

            else if ("requireName".equals(propertyName)) {
              return requireNameForType(entity.getTypeName());
            }

            else if ("properties".equals(propertyName)) {

              Map propertyMap = new HashMap();
              for (Property p : entity.getProperties()) {
                propertyMap.put(p.getName(), p);
              }

              List sortedProperties = new ArrayList();
              sortedProperties.addAll(propertyMap.values());
              Collections.sort(sortedProperties, new Comparator() {
                @Override
                public int compare(Property p1, Property p2) {
                  return p1.getName().compareTo(p2.getName());
                }
              });

              return sortedProperties;
            }

            else if ("entityProperties".equals(propertyName)) {

              Map propertyMap = new HashMap();
              for (Property p : entity.getProperties()) {

                // TODO if we decide to encode enum types, we'll want to remove the second condition
                if (p.getType().getName() != null) {
                  propertyMap.put(p.getName(), p);
                }
              }

              List sortedProperties = new ArrayList();
              sortedProperties.addAll(propertyMap.values());
              Collections.sort(sortedProperties, new Comparator() {
                @Override
                public int compare(Property p1, Property p2) {
                  return p1.getName().compareTo(p2.getName());
                }
              });

              return sortedProperties;
            }

            else if ("collectionProperties".equals(propertyName)) {
              List properties = new ArrayList();
              for (Property p : entity.getProperties()) {
                if (p.getType().getJsonKind().equals(JsonKind.LIST)) {
                  properties.add(p);
                }
              }
              List sortedProperties = new ArrayList();
              sortedProperties.addAll(properties);
              Collections.sort(sortedProperties, new Comparator() {
                @Override
                public int compare(Property p1, Property p2) {
                  return p1.getName().compareTo(p2.getName());
                }
              });
              return sortedProperties;
            }

            return super.getProperty(interp, self, o, property, propertyName);
          }
        });

    group.registerModelAdaptor(ParameterDescription.class,
        new ObjectModelAdaptor() {
          @Override
          public Object getProperty(Interpreter interp, ST self, Object o,
              Object property, String propertyName)
              throws STNoSuchPropertyException {
            ParameterDescription param = (ParameterDescription) o;
            if ("requireName".equals(propertyName)) {
              return upcase(param.getName());
            }
            return super.getProperty(interp, self, o, property, propertyName);
          }
        });

    group.registerModelAdaptor(Property.class, new ObjectModelAdaptor() {
      @Override
      public Object getProperty(Interpreter interp, ST self, Object o,
          Object property, String propertyName)
          throws STNoSuchPropertyException {
        Property p = (Property) o;
        if ("docString".equals(propertyName)) {
          String docString = p.getDocString();

          List enumValues = p.getType().getEnumValues();
          if (enumValues != null) {
            docString = docString == null ? "" : docString;
            docString += "\n\nPossible values: ";
            for (int i = 0; i < enumValues.size(); i++) {
              docString += enumValues.get(i);
              if (i < enumValues.size() - 1) docString += ", ";
            }
          }
          return doxygenDocString(docString);
        }
        else if ("requireName".equals(propertyName)) {
          return requireNameForType(p.getType().getName());
        }
        else if ("objcType".equals(propertyName)) {
          return objcTypeForProperty(p);
        }
        else if ("modifiers".equals(propertyName)) {
          List modifiers = new ArrayList();
          String safeName = getSafeName(p.getName());
          if (p.getImpliedProperty() != null
            && p.getImpliedProperty().getType().getJsonKind().equals(JsonKind.LIST)) {
            modifiers.add("weak");
          }
          else {
            modifiers.add("strong");
          }
          if (p.getType().getJsonKind().equals(JsonKind.BOOLEAN)) {
            modifiers.add("getter=is" + upcase(safeName));
          }
          // http://developer.apple.com/library/ios/#documentation/Cocoa/Conceptual/MemoryMgmt/Articles/mmRules.html
          if (p.getName().startsWith("new")) {
            modifiers.add("getter=a" + upcase(safeName));
          }
          return modifiers;
        }
        else if ("safeName".equals(propertyName)) {
          return getSafeName(p.getName());
        }
        else if ("upcaseName".equals(propertyName)) {
          return upcase(getSafeName(p.getName()));
        }
        else if ("listElementObjcType".equals(propertyName)) {
          return objcTypeForType(p.getType().getListElement());
        }
        else if ("singularUpcaseName".equals(propertyName)) {
          if (p.getType().getListElement() != null) {
            String type = objcTypeForType(p.getType().getListElement());
            return type.substring(2);
          }
          return upcase(p.getName());
        }

        return super.getProperty(interp, self, o, property, propertyName);
      }
    });

    group.registerModelAdaptor(Type.class, new ObjectModelAdaptor() {

      @Override
      public Object getProperty(Interpreter interp, ST self, Object o,
          Object property, String propertyName)
          throws STNoSuchPropertyException {

        if (propertyName.equals("name")) {
          return ((Type) o).getName();
        }
        return super.getProperty(interp, self, o, property, propertyName);
      }
    });

    group.registerModelAdaptor(String.class, new ObjectModelAdaptor() {

      @Override
      public Object getProperty(Interpreter interp, ST self, Object o,
          Object property, String propertyName)
          throws STNoSuchPropertyException {
        final String string = (String) o;
        if ("chunks".equals(propertyName)) {
          /*
           * Split a string into individual chunks that can be reflowed. This implementation is
           * pretty simplistic, but helps make the generated documentation at least somewhat more
           * readable.
           */
          return new Iterator() {
            int index;
            int length = string.length();
            CharSequence next;

            {
              advance();
            }

            @Override
            public boolean hasNext() {
              return next != null;
            }

            @Override
            public CharSequence next() {
              CharSequence toReturn = next;
              advance();
              return toReturn;
            }

            @Override
            public void remove() {
              throw new UnsupportedOperationException();
            }

            private void advance() {
              int start = advance(false);
              int end = advance(true);
              if (start == end) {
                next = null;
              } else {
                next = string.subSequence(start, end);
              }
            }

            /**
             * Advance to next non-whitespace character.
             */
            private int advance(boolean whitespace) {
              while (index < length
                && (whitespace ^ Character.isWhitespace(string.charAt(index)))) {
                index++;
              }
              return index;
            }
          };
        }
        return super.getProperty(interp, self, o, property, propertyName);
      }
    });

    Map namesMap = new HashMap();
    namesMap.put("classPrefix", classPrefix);
    group.defineDictionary("names", namesMap);

    return group;
  }

  private String objcFlatpackReturnType(Type type) {
    String returnType = "void";
    if (type != null) {
      if (type.getListElement() != null) {
        returnType = "NSArray *";
      }
      else if (type.getMapKey() != null) {
        returnType = "NSDictionary *";
      }
      else if (type.getName() != null) {
        returnType = objcTypeForType(type) + " *";
      }
    }
    return returnType;
  }

  private String objcTypeForProperty(Property p) {
    if (p.isEmbedded()) {
      return classPrefix + upcase(p.getType().getName());
    }

    return objcTypeForType(p.getType());
  }

  private String objcTypeForType(Type type) {

    if (type.getName() != null) {
      // if the type is an enum, the type will be a string
      if (type.getEnumValues() != null) {
        return "NSString";
      }

      String name = type.getName();
      String prefix = classPrefix;
      if (name.equalsIgnoreCase("baseHasUuid")) {
        prefix = "FP";
      }
      return prefix + upcase(type.getName());
    }

    String objcType = "nil";
    switch (type.getJsonKind()) {
      case BOOLEAN:
        objcType = "NSNumber";
        break;
      case DOUBLE:
        objcType = "NSNumber";
        break;
      case ANY:
        objcType = "NSObject";
        break;
      case INTEGER:
        objcType = "NSNumber";
        break;
      case LIST:
        objcType = "NSMutableArray";
        break;
      case MAP:
        objcType = "NSMutableDictionary";
        break;
      case NULL:
        objcType = "nil";
        break;
      case STRING:
        objcType = "NSString";
        break;
      default:
        break;
    }

    return objcType;

  }

  private void render(ST enumST, File packageDir, String fileName)
      throws IOException {

    if (!packageDir.isDirectory() && !packageDir.mkdirs()) {
      logger
          .error("Could not create output directory {}", packageDir.getPath());
      return;
    }
    Writer fileWriter = new OutputStreamWriter(new FileOutputStream(new File(
        packageDir, classPrefix + fileName)), "UTF8");
    AutoIndentWriter writer = new AutoIndentWriter(fileWriter);
    writer.setLineWidth(80);
    enumST.write(writer);
    fileWriter.close();
  }

  private String requireNameForType(String type) {
    return type.equalsIgnoreCase("baseHasUuid") ? "FP" + upcase(type) : classPrefix + upcase(type);
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy