Java tutorial
/* * Copyright 2013-2015 the original author or authors. * * 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.expedia.seiso.domain.service.impl; import java.util.ArrayList; import java.util.List; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import lombok.NonNull; import lombok.val; import lombok.extern.slf4j.XSlf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.data.repository.CrudRepository; import org.springframework.data.repository.PagingAndSortingRepository; import org.springframework.data.repository.support.Repositories; import org.springframework.stereotype.Service; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.support.TransactionCallback; import org.springframework.transaction.support.TransactionTemplate; import com.expedia.seiso.core.exception.ResourceNotFoundException; import com.expedia.seiso.core.util.CollectionsUtils; import com.expedia.seiso.domain.entity.Item; import com.expedia.seiso.domain.entity.Node; import com.expedia.seiso.domain.entity.key.ItemKey; import com.expedia.seiso.domain.meta.ItemMetaLookup; import com.expedia.seiso.domain.repo.adapter.RepoAdapterLookup; import com.expedia.seiso.domain.service.ItemService; import com.expedia.seiso.domain.service.SaveAllError; import com.expedia.seiso.domain.service.SaveAllResponse; /** * <p> * CRUD (create, read, update, delete) service implementation. * </p> * <p> * To avoid polluting domain code with integration code, notifications don't happen here. Instead, * {@link com.expedia.seiso.gateway.aop.NotificationAspect} handles those. * </p> * * @author Willie Wheeler */ @Service @Transactional(readOnly = true) @XSlf4j public class ItemServiceImpl implements ItemService { @Autowired private Repositories repositories; @Autowired private RepoAdapterLookup repoAdapterLookup; @Autowired private ItemMetaLookup itemMetaLookup; @Autowired private ItemMerger itemMerger; @Autowired private ItemDeleter itemDeleter; @Autowired private ItemSaver itemSaver; @Autowired private TransactionTemplate txTemplate; @PersistenceContext private EntityManager entityManager; /** * Using {@link Propagation.NEVER} because we don't want a single error to wreck the entire operation. */ @Override @Transactional(propagation = Propagation.NEVER) public SaveAllResponse saveAll(@NonNull Class itemClass, @NonNull List<? extends Item> items, boolean mergeAssociations) { val numItems = items.size(); val itemClassName = itemClass.getSimpleName(); log.info("Batch saving {} items ({})", numItems, itemClass.getSimpleName()); val errors = new ArrayList<SaveAllError>(); for (val item : items) { try { // Have to doInTransaction() since calling save() happens behind the transactional proxy. // Also, see http://stackoverflow.com/questions/5568409/java-generics-void-void-types txTemplate.execute(new TransactionCallback<Void>() { @Override public Void doInTransaction(TransactionStatus status) { save(item, mergeAssociations); return null; } }); } catch (RuntimeException e) { e.printStackTrace(); val message = e.getClass() + ": " + e.getMessage(); errors.add(new SaveAllError(item.itemKey(), message)); } } val numErrors = errors.size(); if (numErrors == 0) { log.info("Batch saved {} items ({}) with no errors", numItems, itemClassName); } else { log.warn("Batch saved {} items ({}) with {} errors: {}", numItems, itemClassName, numErrors, errors); } return new SaveAllResponse(numItems, numErrors, errors); } @Override @Transactional(readOnly = false) public void save(@NonNull Item itemData, boolean mergeAssociations) { val itemKey = itemData.itemKey(); if (itemKey == null) { // No key, so the item is new. itemSaver.create(itemData, mergeAssociations); } else { val itemToSave = doFind(itemKey); if (itemToSave == null) { // Has key, but item is new. itemSaver.create(itemData, mergeAssociations); } else { // Item already exists in database. if (itemData instanceof Node) { // Special logic to handle diamond dependencies per https://github.com/ExpediaDotCom/seiso/issues/33. // When moving a node, we need to delete the old node and create the new node. The deletion and creation // automatically cascades to node IP addresses and endpoints. val oldNode = (Node) itemToSave; val oldSiKey = oldNode.getServiceInstance().getKey(); val newNode = (Node) itemData; val newNodeName = newNode.getName(); val newSiKey = newNode.getServiceInstance().getKey(); log.trace("oldServiceInstance={}, newServiceInstance={}", oldSiKey, newSiKey); // Sanity check if (oldSiKey == null || newSiKey == null) { throw new IllegalStateException("Node save failed: null service instance key"); } if (newSiKey.equals(oldSiKey)) { log.trace("Updating node: {}", newNode); itemSaver.update(newNode, oldNode, mergeAssociations); } else { log.trace("Moving node {} from service instance {} to service instance {}", newNodeName, oldSiKey, newSiKey); log.trace("Deleting node: {}", oldNode.getId()); itemDeleter.delete(oldNode); // Hibernate reorders the operations, performing inserts before deletes, so we have to flush the // session to force the deletes to happen. See https://forum.hibernate.org/viewtopic.php?t=934483. // Note that Gavin says that usually when you delete and then reinsert, you're usually doing it // wrong, but I don't think that's the case here. Our delete cascades down to node IP addresses and // endpoints, and our reinsertion creates new entities through JPA listeners. So we really do want // to wipe out the old entity (or at least its dependencies). [WLW] entityManager.flush(); log.trace("Creating node: {}", newNode.getId()); itemSaver.create(newNode, mergeAssociations); } } else { itemSaver.update(itemData, itemToSave, mergeAssociations); } } } } @Override @SuppressWarnings("rawtypes") public List findAll(@NonNull Class itemClass) { val repo = getRepositoryFor(itemClass); val items = repo.findAll(); return CollectionsUtils.toList(items); } @Override @SuppressWarnings("rawtypes") public Page findAll(@NonNull Class itemClass, @NonNull Pageable pageable) { val repo = (PagingAndSortingRepository) getRepositoryFor(itemClass); return repo.findAll(pageable); } @Override public Item find(@NonNull ItemKey key) { val item = doFind(key); if (item == null) { throw new ResourceNotFoundException(key); } return item; } @Override @Transactional(readOnly = false) public void delete(@NonNull Item item) { itemDeleter.delete(item); } @Override @Transactional(readOnly = false) public void delete(@NonNull ItemKey key) { // We look up the actual item here, as opposed to simply calling a delete(key) method, because we want to throw // a ResourceNotFoundException if the item doesn't exist. itemDeleter.delete(find(key)); } @SuppressWarnings({ "rawtypes", "unchecked" }) private CrudRepository getRepositoryFor(Class<?> itemClass) { return (CrudRepository<?, Long>) repositories.getRepositoryFor(itemClass); } private Item doFind(ItemKey key) { return repoAdapterLookup.getRepoAdapterFor(key.getItemClass()).find(key); } }