/*
 * Copyright 2008 Google 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.
 */
package com.google.gwt.user.server.rpc;

import com.google.gwt.user.server.rpc.impl.StandardSerializationPolicy;
import com.google.gwt.user.server.rpc.impl.TypeNameObfuscator;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.text.ParseException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

/**
 * API for loading a {@link SerializationPolicy}.
 */
public final class SerializationPolicyLoader {

  /**
   * Keyword for listing the serializable fields of an enhanced class that are
   * visible to client code.
   */
  public static final String CLIENT_FIELDS_KEYWORD = "@ClientFields";

  /**
   * Keyword for final field serialization strategy.
   */
  public static final String FINAL_FIELDS_KEYWORD = "@FinalFields";

  /**
   * Default encoding for serialization policy files.
   */
  public static final String SERIALIZATION_POLICY_FILE_ENCODING = "UTF-8";

  /**
   * System property to enable gwt-rpc enhanced classes. To use this, set the JVM system property
   * with name {@value ENABLE_GWT_ENHANCED_CLASSES_PROPERTY} to {@code true}, any other value will
   * leave this feature disabled in the server at runtime.
   */
  public static final String ENABLE_GWT_ENHANCED_CLASSES_PROPERTY = "gwt.enhancedClasses.enabled";

  /**
   * Flag to enable using enhanced classes, for applications that need them and are taking
   * appropriate steps to secure them. Defaults to false.
   */
  public static final boolean ENABLE_ENHANCED_CLASSES = "true".equals(System.getProperty(ENABLE_GWT_ENHANCED_CLASSES_PROPERTY));

  private static final String FORMAT_ERROR_MESSAGE = "Expected: className, "
      + "[true | false], [true | false], [true | false], [true | false], typeId, signature";
  
  /**
   * Returns the serialization policy file name from the serialization
   * policy strong name.
   * 
   * @param serializationPolicyStrongName the serialization policy strong name
   * @return the serialization policy file name from the serialization
   *         policy strong name
   */
  public static String getSerializationPolicyFileName(
      String serializationPolicyStrongName) {
    return serializationPolicyStrongName + ".gwt.rpc";
  }

  /**
   * Loads a SerializationPolicy from an input stream.
   * 
   * @param inputStream stream to load from
   * @return a {@link SerializationPolicy} loaded from the input stream
   * 
   * @throws IOException if an error occurs while reading the stream
   * @throws ParseException if the input stream is not properly formatted
   * @throws ClassNotFoundException if a class specified in the serialization
   *           policy cannot be loaded
   * 
   * @deprecated see {@link #loadFromStream(InputStream, List)}
   */
  @Deprecated
  public static SerializationPolicy loadFromStream(InputStream inputStream)
      throws IOException, ParseException, ClassNotFoundException {
    List<ClassNotFoundException> classNotFoundExceptions = new ArrayList<ClassNotFoundException>();
    SerializationPolicy serializationPolicy = loadFromStream(inputStream,
        classNotFoundExceptions);
    if (!classNotFoundExceptions.isEmpty()) {
      // Just report the first failure.
      throw classNotFoundExceptions.get(0);
    }

    return serializationPolicy;
  }

  /**
   * Loads a SerializationPolicy from an input stream and optionally record any
   * {@link ClassNotFoundException}s.
   * 
   * @param inputStream stream to load the SerializationPolicy from.
   * @param classNotFoundExceptions if not <code>null</code>, all of the
   *          {@link ClassNotFoundException}s thrown while loading this
   *          serialization policy will be added to this list
   * @return a {@link SerializationPolicy} loaded from the input stream.
   * 
   * @throws IOException if an error occurs while reading the stream
   * @throws ParseException if the input stream is not properly formatted
   */
  public static SerializationPolicy loadFromStream(InputStream inputStream,
      List<ClassNotFoundException> classNotFoundExceptions) throws IOException,
      ParseException {

    if (inputStream == null) {
      throw new NullPointerException("inputStream");
    }

    Map<Class<?>, Boolean> whitelistSer = new HashMap<Class<?>, Boolean>();
    Map<Class<?>, Boolean> whitelistDeser = new HashMap<Class<?>, Boolean>();
    Map<Class<?>, String> typeIds = new HashMap<Class<?>, String>();
    Map<Class<?>, Set<String>> clientFields = new HashMap<Class<?>, Set<String>>();
    boolean shouldSerializeFinalFields = false;

    ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();

    InputStreamReader isr = new InputStreamReader(inputStream,
        SERIALIZATION_POLICY_FILE_ENCODING);
    BufferedReader br = new BufferedReader(isr);

    String line = br.readLine();
    int lineNum = 1;
    while (line != null) {
      line = line.trim();
      if (line.length() > 0) {
        String[] components = line.split(",");

        if (components[0].equals(CLIENT_FIELDS_KEYWORD)) {
          /*
           * Lines starting with '@ClientFields' list potentially serializable fields known to
           * client code for classes that may be enhanced with additional fields on the server.
           * If additional server  fields are found, they will be serizalized separately from the
           * normal RPC process and transmitted to the client as an opaque blob of data stored
           * in a WeakMapping associated with the object instance.
           */
          String binaryTypeName = components[1].trim();
          Class<?> clazz;
          try {
            clazz = Class.forName(binaryTypeName, false, contextClassLoader);
            HashSet<String> fieldNames = new HashSet<String>();
            for (int i = 2; i < components.length; i++) {
              fieldNames.add(components[i]);
            }
            clientFields.put(clazz, fieldNames);
          } catch (ClassNotFoundException ex) {
            // Ignore the error, but add it to the list of errors if one was
            // provided.
            if (classNotFoundExceptions != null) {
              classNotFoundExceptions.add(ex);
            }
          }
        } else if (components[0].equals(FINAL_FIELDS_KEYWORD)) {
          shouldSerializeFinalFields = Boolean.valueOf(components[1].trim());
        } else {
          if (components.length != 2 && components.length != 7) {
            throw new ParseException(FORMAT_ERROR_MESSAGE, lineNum);
          }

          for (int i = 0; i < components.length; i++) {
            components[i] = components[i].trim();
            if (components[i].length() == 0) {
              throw new ParseException(FORMAT_ERROR_MESSAGE, lineNum);
            }
          }

          String binaryTypeName = components[0].trim();
          boolean fieldSer;
          boolean instantSer;
          boolean fieldDeser;
          boolean instantDeser;
          String typeId;

          if (components.length == 2) {
            fieldSer = fieldDeser = true;
            instantSer = instantDeser = Boolean.valueOf(components[1]);
            typeId = binaryTypeName;
          } else {
            int idx = 1;
            // TODO: Validate the instantiable string better.
            fieldSer = Boolean.valueOf(components[idx++]);
            instantSer = Boolean.valueOf(components[idx++]);
            fieldDeser = Boolean.valueOf(components[idx++]);
            instantDeser = Boolean.valueOf(components[idx++]);
            typeId = components[idx++];

            if (!fieldSer && !fieldDeser
                && !TypeNameObfuscator.SERVICE_INTERFACE_ID.equals(typeId)) {
              throw new ParseException("Type " + binaryTypeName
                  + " is neither field serializable, field deserializable "
                  + "nor the service interface", lineNum);
            }
          }

          try {
            Class<?> clazz = Class.forName(binaryTypeName, false,
                contextClassLoader);
            if (fieldSer) {
              whitelistSer.put(clazz, instantSer);
            }
            if (fieldDeser) {
              whitelistDeser.put(clazz, instantDeser);
            }
            typeIds.put(clazz, typeId);
          } catch (ClassNotFoundException ex) {
            // Ignore the error, but add it to the list of errors if one was
            // provided.
            if (classNotFoundExceptions != null) {
              classNotFoundExceptions.add(ex);
            }
          }
        }
      }

      line = br.readLine();
      lineNum++;
    }

    return new StandardSerializationPolicy(whitelistSer, whitelistDeser,
        typeIds, clientFields, shouldSerializeFinalFields);
  }

  private SerializationPolicyLoader() {
  }
}
