Package org.yaac.server.service

Source Code of org.yaac.server.service.CRUDServiceImpl

package org.yaac.server.service;

import static com.google.common.base.Strings.isNullOrEmpty;
import static com.google.common.collect.Lists.newLinkedList;
import static com.google.common.collect.Lists.transform;
import static com.google.common.collect.Maps.newHashMap;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.inject.Inject;

import org.antlr.runtime.RecognitionException;
import org.yaac.client.service.CRUDService;
import org.yaac.server.delegate.MemcacheServiceDelegate;
import org.yaac.server.egql.EGQLUtil;
import org.yaac.server.egql.evaluator.EvaluationResult;
import org.yaac.server.egql.evaluator.Evaluator;
import org.yaac.server.egql.processor.ProcessData.ProcessDataRecord;
import org.yaac.server.util.AutoBeanUtil;
import org.yaac.server.util.DatastoreUtil;
import org.yaac.shared.ErrorCode;
import org.yaac.shared.SharedConstants.Datastore;
import org.yaac.shared.YaacException;
import org.yaac.shared.crud.MetaNamespace;
import org.yaac.shared.editor.EntityHierarchy;
import org.yaac.shared.editor.EntityInfo;
import org.yaac.shared.editor.EntityUpdateInfo;
import org.yaac.shared.editor.PropertyUpdateInfo;
import org.yaac.shared.file.FileDownloadPath;
import org.yaac.shared.property.BlobKeyPropertyInfo;
import org.yaac.shared.property.BlobPropertyInfo;
import org.yaac.shared.property.BooleanPropertyInfo;
import org.yaac.shared.property.CategoryPropertyInfo;
import org.yaac.shared.property.DatePropertyInfo;
import org.yaac.shared.property.DoublePropertyInfo;
import org.yaac.shared.property.EmailPropertyInfo;
import org.yaac.shared.property.GeoPtPropertyInfo;
import org.yaac.shared.property.KeyInfo;
import org.yaac.shared.property.LinkPropertyInfo;
import org.yaac.shared.property.ListPropertyInfo;
import org.yaac.shared.property.LongPropertyInfo;
import org.yaac.shared.property.PhoneNumberPropertyInfo;
import org.yaac.shared.property.PostalAddressPropertyInfo;
import org.yaac.shared.property.PropertyInfo;
import org.yaac.shared.property.PropertyType;
import org.yaac.shared.property.ShortBlobPropertyInfo;
import org.yaac.shared.property.StringPropertyInfo;
import org.yaac.shared.property.TextPropertyInfo;
import org.yaac.shared.property.UserPropertyInfo;

import com.google.appengine.api.NamespaceManager;
import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.datastore.AsyncDatastoreService;
import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.Category;
import com.google.appengine.api.datastore.DatastoreNeedIndexException;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Email;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.EntityNotFoundException;
import com.google.appengine.api.datastore.FetchOptions;
import com.google.appengine.api.datastore.GeoPt;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.appengine.api.datastore.Link;
import com.google.appengine.api.datastore.PhoneNumber;
import com.google.appengine.api.datastore.PostalAddress;
import com.google.appengine.api.datastore.Query;
import com.google.appengine.api.datastore.ShortBlob;
import com.google.appengine.api.datastore.Text;
import com.google.appengine.api.users.User;
import com.google.common.base.Function;

import com.googlecode.gql4j.GqlQuery;

/**
* @author Max Zhu (thebbsky@gmail.com)
*
*/
public class CRUDServiceImpl implements CRUDService {

  private final Logger logger;
 
  private final MemcacheServiceDelegate memcache;
 
  @Inject
  CRUDServiceImpl(Logger logger, MemcacheServiceDelegate memcache) {
    super();
    this.logger = logger;
    this.memcache = memcache;
  }

  @Override
  public List<MetaNamespace> loadMetaData() {
    AsyncDatastoreService datastore = DatastoreServiceFactory.getAsyncDatastoreService();
   
    // step 1 : load all namespaces
    Query nsQ = new Query(Query.NAMESPACE_METADATA_KIND).setKeysOnly();
    Iterable<Entity> namespaces = datastore.prepare(nsQ).asIterable();
   
    // step 2 : load all kinds and properties per each namespaces
    String defaultNs = NamespaceManager.get();
   
    Map<String, Iterable<Entity>> kindsMap = new HashMap<String, Iterable<Entity>>();
    Map<String, Iterable<Entity>> propertiesMap = new HashMap<String, Iterable<Entity>>();
    for (Entity e : namespaces) {
      String namespace = e.getKey().getName();
     
      if (isNullOrEmpty(namespace)) {
        NamespaceManager.set(defaultNs);
      } else {
        NamespaceManager.set(namespace);
      }
     
      Query kindQ = new Query(Query.KIND_METADATA_KIND).setKeysOnly();
      kindsMap.put(namespace, datastore.prepare(kindQ).asIterable());
     
      // ancestor query is not used here, properties will be grouped in memory
      // not keyOnly query because we will need to load property_representation
      Query propertyQ = new Query(Query.PROPERTY_METADATA_KIND);
      propertiesMap.put(namespace, datastore.prepare(propertyQ).asIterable());
    }
   
    // set namespace back
    NamespaceManager.set(defaultNs);
   
    // step 3 : prepare results   
    return DatastoreUtil.buildMetaData(kindsMap, propertiesMap);
  }

  @Override
  public List<EntityInfo> loadEntities(String kind, String filter, int start, int length
      ) throws YaacException {
    DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
   
    Query q = isNullOrEmpty(filter) ?
      new Query(kind) : new GqlQuery("select * from " + kind + " " + filter).query();
   
    FetchOptions options = FetchOptions.Builder.withLimit(length).offset(start);
   
    try {
      Iterable<Entity> entities = datastore.prepare(q).asIterable(options);
     
      List<EntityInfo> result = new LinkedList<EntityInfo>();
      for (Entity e : entities) {
        result.add(DatastoreUtil.convert(e));
      }
     
      return result; 
    } catch (DatastoreNeedIndexException e) {
      logger.log(Level.INFO,
          "exception when loading entities, " +
          " kind = " + kind +
          " filter = " + filter +
          " start = " + start +
          " length = " + length, e);
     
      throw new YaacException(null, e.getMissingIndexDefinitionXml());
    }
  }

  @Override
  public EntityHierarchy loadEntityHierarchy(String keyString) throws YaacException {   
    // precondition checking
    if (isNullOrEmpty(keyString)) {
      logger.info("Empty key string");
      return null;
    }
   
    try {
      Key key = null;
      try {
        EvaluationResult r = EGQLUtil.parser(keyString).bool_exp().e.evaluate((ProcessDataRecord)null);
        key = (Key) r.getPayload();
      } catch (Exception e) {
        logger.info("failed to load key in formatted version, fallback to parse as keyString");
        // any exception, fallback
        key = KeyFactory.stringToKey(keyString);
      }
     
      // load data
      DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
      Map<Key, Entity> resultMap = datastore.get(DatastoreUtil.withAllAncesterKeys(key));
     
      EntityHierarchy hierarchy = new EntityHierarchy(
          DatastoreUtil.convert(key), DatastoreUtil.convert(resultMap.get(key)));
     
      while (key.getParent() != null) {
        key = key.getParent();
        hierarchy.put(DatastoreUtil.convert(key), DatastoreUtil.convert(resultMap.get(key)));
      }
     
      return hierarchy; 
    } catch (IllegalArgumentException e) {
      // invalid key string
      throw new YaacException(null, "Invalid key string");
    }
  }

  @Override
  public void updateEntity(Collection<EntityUpdateInfo> infos) {   
    DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
   
    Map<Key, EntityUpdateInfo> infoMap = newHashMap();
   
    // step 1 : load existing entities from datastore
    List<Key> keys = new ArrayList<Key>(infos.size());
    for (EntityUpdateInfo info : infos) {
      Key key = DatastoreUtil.convert(info.getOrigKey());
      keys.add(key);
      infoMap.put(key, info);
    }
   
    Map<Key, Entity> entityMap  = datastore.get(keys);
   
    // step 2 : update
    List<Entity> updateList = newLinkedList();
    for (Key key : infoMap.keySet()) {
      EntityUpdateInfo info = infoMap.get(key);
      Entity entity = entityMap.get(key);
     
      updateList.add(applyChange(entity, info.getChanges()));
    }   
   
    datastore.put(updateList);
  }
 
  /**
   * @param entity
   * @param changes
   * @return
   */
  Entity applyChange(Entity entity, List<PropertyUpdateInfo> changes) {
    for (PropertyUpdateInfo change : changes) {
      if (Datastore.KEY_RESERVED_NAME.equals(change.getName())) {
        // key is changed
        // clone properties to new entity
        Key newKey = DatastoreUtil.convert((KeyInfo)change.getNewInfo());
        Entity newEntity = new Entity(newKey);
        newEntity.setPropertiesFrom(entity);
       
        // remove existing entity
        DatastoreServiceFactory.getAsyncDatastoreService().delete(entity.getKey());
        entity = newEntity;
      } else {
        if (change.isDeleteFlag()) {
          // simply delete the property
          entity.removeProperty(change.getName());
          continue;
        }
       
        // change name
        if (change.getNewName() != null) {
          Object value = entity.getProperty(change.getName());
          entity.removeProperty(change.getName());
          entity.setProperty(change.getNewName(), value);
        }
       
        if (change.getNewInfo() != null) { // value changed
          entity.setProperty(change.getNewName() == null ? change.getName() : change.getNewName(),
              convert(change.getNewInfo(), entity));
        }
      }
    }
   
    return entity;
  }
 
  /**
   * convert from PropertyInfo to object value
   *
   * @param info
   * @return
   */
  @SuppressWarnings({ "rawtypes", "unchecked" })
  private Object convert(PropertyInfo info, Entity e) {
    PropertyType type = PropertyType.typeOf(info);
   
    switch (type) {
    case BLOB:
      BlobPropertyInfo blobInfo = (BlobPropertyInfo) info;
     
      if (blobInfo.getRawData() == null) {
        FileDownloadPath path = AutoBeanUtil.decode(FileDownloadPath.class, blobInfo.getDownloadPath());

        switch (path.getType()) {
        case MEMCACHE:
          byte [] data = (byte[]) memcache.get(path.getKeyStr());
          // throw exception when rawData == null? cache get expired is possible
          return new Blob(data);
        case DATASTORE_BLOB:
          // simply return existing one
          Object val = e.getProperty(path.getFieldName());
          if (val instanceof Blob) {
            return val;
          } else if (val instanceof List) {
            return ((List)val).get(path.getIndex());
          } else {
            // should not happen
            throw new IllegalArgumentException("Can't locate blob via path " + path.getFieldName() + " " + path.getIndex());
          }
        default:
          // should not happen
          throw new IllegalArgumentException("Can't locate blob via path " + path.getFieldName() + " " + path.getIndex());
        }
      } else {
        // edit blob value directly
        return new Blob(blobInfo.getRawData());
      }
    case BLOB_KEY:
      return new BlobKey(((BlobKeyPropertyInfo) info).getBlobKey());
    case BOOL:
      return ((BooleanPropertyInfo) info).getPayload();
    case CATEGORY:
      return new Category(((CategoryPropertyInfo) info).getPayload());
    case DOUBLE:
      return ((DoublePropertyInfo) info).getPayload();
    case EMAIL:
      return new Email(((EmailPropertyInfo) info).getPayload());
    case GEOPT:
      GeoPtPropertyInfo geoPtInfo = (GeoPtPropertyInfo) info;
      return new GeoPt(geoPtInfo.getLatitude(), geoPtInfo.getLongitude());
    case IM_HANDLE:
      // TODO
      return null;
    case KEY:
      return DatastoreUtil.convert((KeyInfo)info);
    case LINK:
      return new Link(((LinkPropertyInfo) info).getPayload());
    case LIST:
      ListPropertyInfo listInfo = (ListPropertyInfo) info;
      List list = newLinkedList();
      for (PropertyInfo i : listInfo.getPayload()) {
        list.add(convert(i, e));
      }
      return list;
    case LONG:
      return ((LongPropertyInfo) info).getPayload();
    case NULL:
      return null;
    case PHONE_NO:
      return new PhoneNumber(((PhoneNumberPropertyInfo) info).getPayload());
    case POSTTAL_ADDRESS:
      return new PostalAddress(((PostalAddressPropertyInfo) info).getPayload());
    case SHORT_BLOB:
      return new ShortBlob(((ShortBlobPropertyInfo) info).getPayload().getBytes());
    case STRING:
      return ((StringPropertyInfo) info).getPayload();
    case TEXT:     
      TextPropertyInfo textInfo = (TextPropertyInfo) info;
     
      if (textInfo.getRawText() == null) {
        FileDownloadPath path = AutoBeanUtil.decode(FileDownloadPath.class, textInfo.getDownloadPath());

        switch (path.getType()) {
        case MEMCACHE:
          byte [] data = (byte[]) memcache.get(path.getKeyStr());
          // throw exception when rawData == null? cache get expired is possible
          return new Text(new String(data));
        case DATASTORE_TEXT:
          // simply return existing one
          Object val = e.getProperty(path.getFieldName());
          if (val instanceof Text) {
            return val;
          } else if (val instanceof List) {
            return ((List)val).get(path.getIndex());
          } else {
            // should not happen
            throw new IllegalArgumentException("Can't locate text via path " + path.getFieldName() + " " + path.getIndex());
          }
        default:
          // should not happen
          throw new IllegalArgumentException("Can't locate text via path " + path.getFieldName() + " " + path.getIndex());
        }
      } else {
        // edit text value directly
        return new Text(textInfo.getRawText());
      }
    case TIMESTAMP:
      return ((DatePropertyInfo) info).getPayload();
    case USER:
      UserPropertyInfo upi = (UserPropertyInfo) info;
      // nickname is a derived field, no need to set here
      return new User(upi.getEmail(), upi.getAuthDomain(), upi.getUserId(), upi.getFederatedIdentity());
    default:
      throw new IllegalArgumentException("Invalid type " + info.getClass());
    }
  }

  @Override
  public PropertyInfo parsePropertyExp(String keyString, String exp, List<String> filePaths) throws YaacException {
    try {
      // property can also be used during evaluation
      Evaluator e = EGQLUtil.parser(exp).bool_exp().e;
      if (!e.aggregationChildren().isEmpty()) { 
        // aggregation function is not allowed here
        throw new YaacException(ErrorCode.E301, null);
      }
     
      // load targeting entity
      DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
      final Entity entity = datastore.get(KeyFactory.stringToKey(keyString));
     
      // file context
      final List<FileDownloadPath> files = filePaths == null ? new ArrayList<FileDownloadPath>() :
        transform(filePaths, new Function<String, FileDownloadPath>(){
        @Override
        public FileDownloadPath apply(String pathStr) {
          return AutoBeanUtil.decode(FileDownloadPath.class, pathStr);
        }
      });
     
      EvaluationResult r = e.evaluate(new ProcessDataRecord() {
        @Override
        public FileDownloadPath lookupFileReference(Integer index) {
          return files.get(index);
        }
       
        @Override
        public EvaluationResult lookup(String name) {
          if (isNullOrEmpty(name)) {
            return null;
          } else // normal case, include key selection and property selection
            Object payload = Datastore.KEY_RESERVED_NAME.equals(name) ? entity.getKey() : entity.getProperty(name);
            return new EvaluationResult(entity.getKey(), name, null, payload)
          }
        }
       
        @Override
        public Iterable<EvaluationResult> asIterable() {
          // should not be called
          throw new IllegalArgumentException();
        }
      });
     
      PropertyInfo info = DatastoreUtil.convert(keyString, null, null, r.getPayload(), r.getWarnings());
      return info;
    } catch (RecognitionException e) {
      // can't parse input
      logger.log(Level.SEVERE, e.getMessage(), e);
      throw new YaacException(null, e.getMessage());
    } catch (EntityNotFoundException e) {
      logger.log(Level.SEVERE, e.getMessage(), e);
      throw new YaacException(ErrorCode.E302, null);
    }
  }

  @Override
  public void delete(KeyInfo keyInfo) {
    Key key = DatastoreUtil.convert(keyInfo);
    DatastoreServiceFactory.getDatastoreService().delete(key);
  }

  @Override
  public String copyToNew(KeyInfo keyInfo) throws YaacException {   
    try {
      Key key = DatastoreUtil.convert(keyInfo);
      DatastoreService datastore = DatastoreServiceFactory.getDatastoreService();
      Entity from = datastore.get(key);
     
      Entity to = new Entity(key.getKind())// copy kind, but not key name
      to.setPropertiesFrom(from)// copy all properties
      datastore.put(to);
     
      return KeyFactory.keyToString(to.getKey());
    } catch (EntityNotFoundException e) {
      throw new YaacException(null, "Requested entity doesn't exist, please refresh your browser");
    }
  }

  @Override
  public void create(KeyInfo keyInfo) {
    Key key = DatastoreUtil.convert(keyInfo);
    Entity e = new Entity(key);
    DatastoreServiceFactory.getDatastoreService().put(e);
  }
}
TOP

Related Classes of org.yaac.server.service.CRUDServiceImpl

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.