// Copyright 2012 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.enterprise.connector.pusher;

import com.google.common.base.Preconditions;
import com.google.enterprise.connector.spi.Document;
import com.google.enterprise.connector.spi.Property;
import com.google.enterprise.connector.spi.RepositoryException;
import com.google.enterprise.connector.spi.SimpleProperty;
import com.google.enterprise.connector.spi.SkippedDocumentException;
import com.google.enterprise.connector.spi.SpiConstants;
import com.google.enterprise.connector.spi.SpiConstants.DocumentType;
import com.google.enterprise.connector.spi.Value;
import com.google.enterprise.connector.util.filter.DocumentFilterFactory;

import java.util.Collections;
import java.util.HashSet;
import java.util.Set;

/**
 * Dumb-down ACLs unsupported by current GSA. If an ACL feature is used by the
 * document isn't supported by the GSA we are communicated with, then we remove
 * all the document's ACLs and mark the document private to enable late-binding
 * authz.
 */
public class AclDocumentFilter implements DocumentFilterFactory {
  private static final Property PROPERTY_FALSE =
      new SimpleProperty(Value.getBooleanValue(false));
  private static final Set<String> PROPERTIES_UNSUPPORTED;
  private static final Set<String> PROPERTIES_TO_REMOVE;

  static {
    Set<String> properties = new HashSet<String>();
    properties.add(SpiConstants.PROPNAME_ACLINHERITFROM);
    properties.add(SpiConstants.PROPNAME_ACLINHERITFROM_DOCID);
    PROPERTIES_UNSUPPORTED = Collections.unmodifiableSet(properties);

    properties = new HashSet<String>();
    properties.add(SpiConstants.PROPNAME_ACLGROUPS);
    properties.add(SpiConstants.PROPNAME_ACLUSERS);
    properties.add(SpiConstants.PROPNAME_ACLDENYGROUPS);
    properties.add(SpiConstants.PROPNAME_ACLDENYUSERS);
    properties.add(SpiConstants.PROPNAME_ACLINHERITANCETYPE);
    properties.add(SpiConstants.PROPNAME_ACLINHERITFROM);
    properties.add(SpiConstants.PROPNAME_ACLINHERITFROM_DOCID);
    properties.add(SpiConstants.PROPNAME_ACLINHERITFROM_FEEDTYPE);
    PROPERTIES_TO_REMOVE = Collections.unmodifiableSet(properties);
  }

  private final FeedConnection feedConnection;
  /**
   * GSA 7.0 introduced the ability to use the acl tag in feeds and introduced
   * ACL inheritance. For correctness, this flag must be off if these features
   * are unsupported. The meaning of ACLs changes depending on if the GSA
   * supports inheritance, for instance.
   */
  private Boolean useEnhancedAcls;

  public AclDocumentFilter(FeedConnection feedConnection) {
    this.feedConnection = feedConnection;
  }

  /* @Override */
  public Document newDocumentFilter(Document source) {
    if (getUseEnhancedAcls()) {
      return source;
    } else {
      // It may be necessary to modify properties. Wrap it now and determine
      // later what actions (if any) are necessary. We can't determine now what
      // needs to be done because we can't throw a RepositoryException.
      return new LazyDocumentFilter(source);
    }
  }

  private boolean getUseEnhancedAcls() {
    if (useEnhancedAcls != null) {
      return useEnhancedAcls;
    }

    this.useEnhancedAcls = feedConnection.supportsAcls();
    return useEnhancedAcls;
  }

  /**
   * Lazily load real document filter. This is only necessary because
   * newDocumentFilter() can't throw RepositoryException.
   */
  private static class LazyDocumentFilter implements Document {
    private final Document source;
    private Document documentFilter;

    public LazyDocumentFilter(Document source) {
      this.source = source;
    }

    /* @Override */
    public Property findProperty(String name) throws RepositoryException {
      return getDocumentFilter().findProperty(name);
    }

    /* @Override */
    public Set<String> getPropertyNames() throws RepositoryException {
      return getDocumentFilter().getPropertyNames();
    }

    private Document getDocumentFilter() throws RepositoryException {
      if (documentFilter != null) {
        return documentFilter;
      }

      if (isAclDocument(source)) {
        // Throw-away ACL-only documents if the GSA can't handle them. These
        // documents should only be used with ACL inheritance, and since we
        // fallback to non-ACL solutions when ACL inheritance is in place,
        // things should "just work," even though we are throwing away
        // information here.
        documentFilter = new SkipDocument(source);
      } else if (requiresDumbingDown(source)) {
        documentFilter = new NoAclsDocument(source);
      } else {
        documentFilter = source;
      }
      return documentFilter;
    }

    private boolean isAclDocument(Document source) throws RepositoryException {
      String docType = DocUtils.getOptionalString(source,
          SpiConstants.PROPNAME_DOCUMENTTYPE);
      return docType != null
          && DocumentType.findDocumentType(docType) == DocumentType.ACL;
    }

    private boolean requiresDumbingDown(Document source)
        throws RepositoryException {
      Set<String> propertyNames = source.getPropertyNames();
      for (String unsupported : PROPERTIES_UNSUPPORTED) {
        if (propertyNames.contains(unsupported)) {
          return true;
        }
      }
      return false;
    }
  }

  /**
   * A document that always throws {@link SkippedDocumentException}.
   */
  private static class SkipDocument implements Document {
    private Document source;

    public SkipDocument(Document source) {
      this.source = source;
    }

    /* @Override */
    public Property findProperty(String name) throws RepositoryException {
      if (SpiConstants.PROPNAME_DOCID.equals(name)) {
        return source.findProperty(name);
      }
      throw new SkippedDocumentException("Document was an ACL document, which "
          + "is unsupported on this GSA");
    }

    /* @Override */
    public Set<String> getPropertyNames() throws RepositoryException {
      throw new SkippedDocumentException("Document was an ACL document, which "
          + "is unsupported on this GSA");
    }
  }

  /**
   * A document filter that removes ACLs and marks the document as private.
   */
  private static class NoAclsDocument implements Document {
    private final Document source;

    public NoAclsDocument(Document source) {
      this.source = source;
    }

    /* @Override */
    public Property findProperty(String name)
        throws RepositoryException {
      if (name == SpiConstants.PROPNAME_ISPUBLIC) {
        return PROPERTY_FALSE;
      } else if (PROPERTIES_TO_REMOVE.contains(name)) {
        return null;
      } else {
        return source.findProperty(name);
      }
    }

    /* @Override */
    public Set<String> getPropertyNames()
        throws RepositoryException {
      Set<String> names = source.getPropertyNames();
      names = new HashSet<String>(names);
      names.removeAll(PROPERTIES_TO_REMOVE);
      names.add(SpiConstants.PROPNAME_ISPUBLIC);
      return names;
    }
  }
}

@@ -1628,113 +1628,8 @@
 y %7B%0A
-  private static final Property PROPERTY_FALSE =%0A      new SimpleProperty(Value.getBooleanValue(false));%0A
   pr
@@ -6971,22 +6971,56 @@
 urn 
-PROPERTY_FALSE
+new SimpleProperty(Value.getBooleanValue(false))
 ;%0A  

Fix iteration problem with AclDocumentFilter