fr.gael.dhus.service.ProductService.java Source code

Java tutorial

Introduction

Here is the source code for fr.gael.dhus.service.ProductService.java

Source

/*
 * Data Hub Service (DHuS) - For Space data distribution.
 * Copyright (C) 2013,2014,2015,2016 GAEL Systems
 *
 * This file is part of DHuS software sources.
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as
 * published by the Free Software Foundation, either version 3 of the
 * License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */
package fr.gael.dhus.service;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;

import fr.gael.dhus.database.dao.CollectionDao;
import fr.gael.dhus.database.dao.ProductDao;
import fr.gael.dhus.database.dao.UserDao;
import fr.gael.dhus.database.object.Collection;
import fr.gael.dhus.database.object.MetadataIndex;
import fr.gael.dhus.database.object.Product;
import fr.gael.dhus.database.object.User;
import fr.gael.dhus.datastore.DataStore;
import fr.gael.dhus.datastore.Destination;
import fr.gael.dhus.datastore.Ingester;
import fr.gael.dhus.datastore.exception.DataStoreAlreadyExistException;
import fr.gael.dhus.datastore.exception.DataStoreException;
import fr.gael.dhus.datastore.exception.DataStoreLocalArchiveNotExistingException;
import fr.gael.dhus.datastore.processing.fair.FairCallable;
import fr.gael.dhus.datastore.processing.fair.FairThreadPoolTaskExecutor;
import fr.gael.dhus.datastore.scanner.AsynchronousLinkedList;
import fr.gael.dhus.datastore.scanner.AsynchronousLinkedList.Event;
import fr.gael.dhus.datastore.scanner.AsynchronousLinkedList.Listener;
import fr.gael.dhus.datastore.scanner.FileScannerWrapper;
import fr.gael.dhus.datastore.scanner.Scanner;
import fr.gael.dhus.datastore.scanner.ScannerFactory;
import fr.gael.dhus.datastore.scanner.URLExt;
import fr.gael.dhus.search.SolrDao;
import fr.gael.dhus.spring.cache.IncrementCache;
import fr.gael.dhus.spring.context.ApplicationContextProvider;
import fr.gael.dhus.system.config.ConfigurationManager;
import fr.gael.drbx.cortex.DrbCortexItemClass;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.solr.client.solrj.SolrServerException;
import org.hibernate.Hibernate;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.criterion.Projections;
import org.hibernate.criterion.Restrictions;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * Product Service provides connected clients with a set of method
 * to interact with it.
 */
@Service
public class ProductService extends WebService {
    private static final Logger LOGGER = LogManager.getLogger(ProductService.class);

    private static final long DAY_MILLI = 24 * 60 * 60 * 1_000;

    @Autowired
    private ProductDao productDao;

    @Autowired
    private CollectionDao collectionDao;

    @Autowired
    private CollectionService collectionService;

    @Autowired
    private UserDao userDao;

    @Autowired
    private FairThreadPoolTaskExecutor taskExecutor;

    @Autowired
    private ScannerFactory scannerFactory;

    /** Configuration (etc/dhus.xml). */
    @Autowired
    private ConfigurationManager cfgManager;

    @Autowired
    private SolrDao solrDao;

    @Autowired
    private DataStore<Product> dataStore;

    @PreAuthorize("hasRole('ROLE_DATA_MANAGER')")
    public Iterator<Product> GetProducts(String filter, Long collection_id, int skip) {
        return systemGetProducts(filter, collection_id, skip);
    }

    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    public Iterator<Product> systemGetProducts(String filter, Long collection_id, int skip) {
        return productDao.scrollFiltered(filter, collection_id, skip);
    }

    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    @Cacheable(value = "product", key = "#id")
    public Product systemGetProduct(Long id) {
        return productDao.read(id);
    }

    @PreAuthorize("hasAnyRole('ROLE_DATA_MANAGER','ROLE_SEARCH')")
    @Cacheable(value = "product", key = "#id")
    public Product getProduct(Long id) {
        return systemGetProduct(id);
    }

    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    public Product getProductNoCache(Long id) {
        return productDao.read(id);
    }

    @PreAuthorize("hasAnyRole('ROLE_DATA_MANAGER','ROLE_SEARCH')")
    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    @Cacheable(value = "products", key = "#ids")
    public List<Product> getProducts(List<Long> ids) {
        return productDao.read(ids);
    }

    /**
     * Gets a {@link Product} by its {@code UUID} (Protected).
     * @see #getProduct(java.lang.String)
     * @param uuid UUID unique identifier
     * @return a {@link Product} or {@code null}
     */
    @PreAuthorize("hasAnyRole('ROLE_DATA_MANAGER','ROLE_SEARCH')")
    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    @Cacheable(value = "product", key = "#uuid")
    public Product getProduct(String uuid) {
        return systemGetProduct(uuid);
    }

    /**
     * Gets a {@link Product} by its {@code UUID} (Unprotected).
     * @param uuid UUID unique identifier
     * @return a {@link Product} or {@code null}
     */
    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    @Cacheable(value = "product", key = "#uuid")
    public Product systemGetProduct(String uuid) {
        return productDao.getProductByUuid(uuid);
    }

    @PreAuthorize("hasRole('ROLE_DOWNLOAD')")
    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    @Cacheable(value = "product", key = "#id")
    public Product getProductToDownload(Long id) {
        // TODO remove method cause duplicated and not used
        return productDao.read(id);
    }

    @PreAuthorize("hasAnyRole('ROLE_DOWNLOAD','ROLE_SEARCH')")
    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    public InputStream getProductQuickLook(Long id) {
        // TODO remove method cause not used
        Product product = getProduct(id);
        if (!product.getQuicklookFlag())
            return null;

        try {
            return new FileInputStream(product.getQuicklookPath());
        } catch (Exception e) {
            LOGGER.warn("Cannot retrieve Quicklook from product id #" + id, e);
        }
        return null;
    }

    @PreAuthorize("hasAnyRole('ROLE_DOWNLOAD','ROLE_SEARCH')")
    public long getProductQuickLookContentLength(Long id) {
        // TODO remove method cause not used
        return getProduct(id).getQuicklookSize();
    }

    @PreAuthorize("hasAnyRole('ROLE_DOWNLOAD','ROLE_SEARCH')")
    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    public InputStream getProductThumbnail(Long id) {
        // TODO remove method cause not used
        Product product = getProduct(id);
        if (!product.getThumbnailFlag())
            return null;
        try {
            return new FileInputStream(product.getThumbnailPath());
        } catch (Exception e) {
            LOGGER.warn("Cannot retrieve Thumbnail from product id #" + id, e);
        }
        return null;
    }

    @PreAuthorize("hasAnyRole('ROLE_DOWNLOAD','ROLE_SEARCH')")
    public long getProductThumbnailContentLength(Long id) {
        // TODO remove method cause not used
        return getProduct(id).getThumbnailSize();
    }

    /**
     * Returns the number of product belonging to the given Collection.
     * <p><b>This method requires roles ROLE_DATA_MANAGER | ROLE_SEARCH.</b>
     * @param filter an optionnal `where` clause (without the "where" token).
     * @param collection_uuid the `Id` of the parent collection.
     * @return number of Products.
     */
    @PreAuthorize("hasAnyRole('ROLE_DATA_MANAGER','ROLE_SEARCH')")
    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    @Cacheable(value = "product_count", key = "{#filter, #collection_uuid}")
    public Integer count(String filter, String collection_uuid) {
        return productDao.count(filter, collection_uuid);
    }

    @PreAuthorize("hasAnyRole('ROLE_DATA_MANAGER','ROLE_SEARCH')")
    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    @Cacheable(value = "product_count", key = "{#filter, null}")
    public Integer count(String filter) {
        return productDao.count(filter, null);
    }

    /**
     * Deletes references and binaries of product.
     * @param pid product ID.
     */
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    @IncrementCache(name = "product_count", key = "all", value = -1)
    @Caching(evict = { @CacheEvict(value = "indexes", key = "#pid"), @CacheEvict(value = "product", key = "#pid"),
            @CacheEvict(value = "products", allEntries = true) })
    public void systemDeleteProduct(Long pid) {
        Product product = productDao.read(pid);

        if (product == null) {
            throw new DataStoreException("Product #" + pid + " not found in the system.");
        }

        systemDeleteProduct(product, Destination.TRASH);
    }

    /**
     * Deletes a product.
     * @param product product to delete.
     * @param destination destination of product backup.
     */
    @Transactional
    @IncrementCache(name = "product_count", key = "all", value = -1)
    @Caching(evict = { @CacheEvict(value = "product", key = "#product.id"),
            @CacheEvict(value = "product", key = "#product.uuid"),
            @CacheEvict(value = "indexes", key = "#product.id"),
            @CacheEvict(value = "products", allEntries = true) })
    public void systemDeleteProduct(Product product, Destination destination) {
        if (product == null) {
            throw new IllegalArgumentException("Product should not be null");
        }

        if (product.getLocked()) {
            throw new DataStoreException(
                    "Cannot delete product : " + product + ". This product is locked by the system");
        }

        long start = System.currentTimeMillis();
        try {
            try {
                solrDao.remove(product.getId());
            } catch (NullPointerException | IllegalStateException e) {
                LOGGER.warn("Solr not running !", e);
            }
            productDao.delete(product);
            dataStore.remove(product, destination);
            long time = System.currentTimeMillis() - start;

            LOGGER.info("Deletion of product '" + product.getIdentifier() + "' (" + product.getDownloadableSize()
                    + " bytes) successful" + " spent " + time + "ms");
        } catch (SolrServerException | IOException e) {
            LOGGER.error("Deletion of product " + product + " failed", e);
            LOGGER.warn("Please delete manually this product");
        }
    }

    @PreAuthorize("hasRole('ROLE_DATA_MANAGER')")
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    @Caching(evict = { @CacheEvict(value = "product", key = "#pid"),
            @CacheEvict(value = "products", allEntries = true), @CacheEvict(value = "indexes", key = "#pid") })
    @IncrementCache(name = "product_count", key = "all", value = -1)
    public void deleteProduct(Long pid) {
        systemDeleteProduct(pid);
    }

    @PreAuthorize("isAuthenticated ()")
    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    @Cacheable(value = "product", key = "#uuid")
    public Product getProduct(String uuid, User u) {
        return productDao.getProductByUuid(uuid);
    }

    @PreAuthorize("isAuthenticated ()")
    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    @Cacheable(value = "product_count", key = "'all'")
    public int count() {
        return productDao.count();
    }

    public boolean hasAccessToProduct(long user_id, long product_id) {
        return true;
    }

    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    @Cacheable(value = { "indexes" }, key = "#product_id")
    public List<MetadataIndex> getIndexes(Long product_id) {
        Product product = productDao.read(product_id);
        if (product == null)
            return new ArrayList<MetadataIndex>();
        Hibernate.initialize(product.getIndexes());
        return product.getIndexes();
    }

    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    public Product getProductWithIndexes(Long product_id) {
        Product product = productDao.read(product_id);
        if (product == null)
            return null;
        Hibernate.initialize(product.getIndexes());
        return product;
    }

    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    @CacheEvict(value = { "indexes" }, key = "#product_id")
    public void setIndexes(Long product_id, List<MetadataIndex> indexes) {
        Product product = productDao.read(product_id);
        product.setIndexes(indexes);
        productDao.update(product);
    }

    /**
     * Adds a product in the database, the given product will not be queued for
     * processing nor it will be submitted to the search engine.
     * @param product a product to store in the database.
     * @return the created product.
     * @throws IllegalArgumentException incomplete products are not allowed.
     */
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    @Caching(evict = { @CacheEvict(value = "product", allEntries = true),
            @CacheEvict(value = "products", allEntries = true) })
    @IncrementCache(name = "product_count", key = "all", value = 1)
    public Product addProduct(Product product) throws IllegalArgumentException {
        URL path = product.getPath();
        String origin = product.getOrigin();
        if (path == null || origin == null || origin.isEmpty()) {
            throw new IllegalArgumentException("product must have a path and an origin");
        }
        // FIXME do I have to check every field? isn't it done by hibernate based on column constraints?

        Product final_product = this.productDao.create(product);
        return final_product;
    }

    @Caching(evict = { @CacheEvict(value = "product", allEntries = true),
            @CacheEvict(value = "products", allEntries = true) })
    @Transactional
    public Product addProduct(URL path, User owner, String origin) throws DataStoreAlreadyExistException {
        if (productDao.exists(path)) {
            throw new DataStoreAlreadyExistException(
                    "Product \"" + path.toExternalForm() + "\" already present in the system.");
        }

        /* **** CRITICAL SECTION *** */
        /** THIS SECTION SHALL NEVER BE STOPPED BY CNTRL-C OR OTHER SIGNALS */
        /* TODO: check if shutdownHook can protect this section */
        Product product = new Product();
        product.setPath(path);
        product.setOrigin(origin);
        List<User> users = new ArrayList<User>();

        if (owner != null) {
            product.setOwner(owner);
            users.add(userDao.read(owner.getUUID()));
            product.setAuthorizedUsers(new HashSet<User>(users));
        }

        product = productDao.create(product);
        return product;
    }

    /**
     * Process given unprocessed product.
     * @param product to process.
     * @param owner user owning that product.
     * @param collections containing that product.
     * @param scanner
     * @param wrapper
     * @return A future to get notified for the end of the processing.
     *         {@code get()} will return null, see {@link ProcessingCallable#call()}.
     *         May return {@code null} if a RejectedExecutionException has been thrown.
     */
    public Future<Object> processProduct(Product product, User owner, List<Collection> collections, Scanner scanner,
            FileScannerWrapper wrapper) {
        Future<Object> future = null;
        int retry = 10;
        while (retry > 0) {
            try {
                ProcessingCallable pr = new ProcessingCallable(product, owner, collections, scanner, wrapper);
                future = taskExecutor.submit(pr);
                retry = 0;
            } catch (RejectedExecutionException ree) {
                retry--;
                if (retry <= 0)
                    throw ree;
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    LOGGER.warn("Current thread has interrupted by another!", e);
                }
            }
        }
        return future;
    }

    /**
     * Odata dedicated Services
     */
    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    @Cacheable(value = "products", key = "{#criteria, #uuid, #skip, #top}")
    public List<Product> getProducts(DetachedCriteria criteria, String uuid, int skip, int top) {
        if (criteria == null) {
            criteria = DetachedCriteria.forClass(Product.class);
        }

        // get only processed products
        criteria.add(Restrictions.eq("processed", true));

        if (uuid != null) {
            Collection collection = collectionDao.read(uuid);
            if (collection != null) {
                criteria.add(Restrictions.in("id", collectionService.getProductIds(uuid)));
            }
        }

        return productDao.listCriteria(criteria, skip, top);
    }

    /**
     * Odata dedicated Services
     */
    @Transactional(readOnly = true)
    @Cacheable(value = "product_count", key = "{#criteria, #uuid}")
    public int countProducts(DetachedCriteria criteria, String uuid) {
        if (criteria == null) {
            criteria = DetachedCriteria.forClass(Product.class);
        }

        // count only processed products
        criteria.add(Restrictions.eq("processed", true));

        if (uuid != null) {
            List<Long> product_ids = collectionService.getProductIds(uuid);
            criteria.add(Restrictions.in("id", product_ids));
        }
        criteria.setProjection(Projections.rowCount());
        return productDao.count(criteria);
    }

    @Transactional(readOnly = true, propagation = Propagation.REQUIRED)
    @Cacheable(value = "product_count", key = "{#filter, #collection?.getUUID ()}")
    public int count(Collection collection, String filter) {
        if (collection == null) {
            return this.count(filter);
        }
        return this.count(filter, collection.getUUID());
    }

    /**
     * Returns all products not contained in a collection.
     * @return a set of products.
     */
    @Transactional(readOnly = true)
    public Set<Product> getNoCollectionProducts() {
        DetachedCriteria criteria = DetachedCriteria.forClass(Product.class);
        Iterator<Product> it = collectionService.getAllProductInCollection().iterator();

        HashSet<Long> cpid = new HashSet<>();
        while (it.hasNext()) {
            cpid.add(it.next().getId());
        }

        criteria.add(Restrictions.not(Restrictions.in("id", cpid)));
        criteria.add(Restrictions.eq("processed", true));
        return new HashSet<>(productDao.listCriteria(criteria, 0, -1));
    }

    /**
     * Retrieve products ingested at a given date in the given collection.
     *
     * @param date       ingestion date.
     * @param collection the collection where found products.
     * @return a set of researched products.
     */
    @Transactional(readOnly = true)
    public Set<Product> getProductByIngestionDate(Date date, Collection collection) {
        Iterator<Product> it;
        Set<Product> productSet = new HashSet<>();

        if (collection == null) {
            it = getNoCollectionProducts().iterator();
        } else {
            it = collectionService.systemGetCollection(collection.getUUID()).getProducts().iterator();
        }

        while (it.hasNext()) {
            Product p = it.next();
            if (p != null) {
                if (date.getTime() - p.getIngestionDate().getTime() < DAY_MILLI) {
                    productSet.add(p);
                }
            }
        }

        return productSet;
    }

    @Transactional(readOnly = true)
    public Product getProductIdentifier(String identifier) {
        return productDao.getProductByIdentifier(identifier);
    }

    @Transactional
    public void update(Product product) {
        productDao.update(product);
    }

    /*
     * Reported from DefaultDataStrore
     */
    private class ProcessingCallable extends FairCallable {
        Product product;
        User owner;
        Scanner scanner;
        List<Collection> collections;
        FileScannerWrapper wrapper;

        public ProcessingCallable(Product product, User owner, List<Collection> collections, Scanner scanner,
                FileScannerWrapper wrapper) {
            super(scanner == null ? null : scanner.toString());
            this.product = product;
            this.owner = owner;
            this.collections = collections;
            this.scanner = scanner;
            this.wrapper = wrapper;
        }

        @IncrementCache(name = "product_count", key = "all", value = 1)
        public Object call() throws Exception {
            ApplicationContextProvider.getBean(Ingester.class).ingest(product, owner, collections, scanner,
                    wrapper);
            return null;
        }
    }

    // Check if product present is the DB is still present into the repository.
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    public void checkDBProducts() {
        LOGGER.info("Syncing database with repositories...");
        Iterator<Product> products = productDao.getAllProducts();
        while (products.hasNext()) {
            Product product = products.next();
            if (!ProductService.checkUrl(product.getPath())) {
                LOGGER.info("Removing Product " + product.getPath() + " not found in repository.");
                products.remove();
            } else
                LOGGER.info("Product " + product.getPath() + " found in repository.");
        }
    }

    private static boolean checkUrl(URL url) {
        Objects.requireNonNull(url, "`url` parameter must not be null");

        // OData Synchronized product, DELME
        if (url.getPath().endsWith("$value")) {
            // Ignoring ...
            return true;
        }

        // Case of simple file
        try {
            File f = new File(url.toString());
            if (f.exists())
                return true;
        } catch (Exception e) {
            LOGGER.debug("url \"" + url + "\" not formatted as a file");
        }

        // Case of local URL
        try {
            URI local = new File(".").toURI();
            URI uri = local.resolve(url.toURI());
            File f = new File(uri);
            if (f.exists())
                return true;
        } catch (Exception e) {
            LOGGER.debug("url \"" + url + "\" not a local URL");
        }

        // Case of remote URL
        try {
            URLConnection con = url.openConnection();
            con.connect();
            InputStream is = con.getInputStream();
            is.close();
            return true;
        } catch (Exception e) {
            LOGGER.debug("url \"" + url + "\" not a remote URL");
        }
        // Unrecovrable case
        return false;
    }

    /**
     * Performs directory structure scan to retrieve relevant products, and run
     * declared processing.
     *
     * @param archive the archive to be scan.
     * @param productDao Data access object to products.
     * @param indexDao Data access object to index in the products.
     * @throws InterruptedException if user
     */
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    public int processArchiveSync() throws DataStoreLocalArchiveNotExistingException, InterruptedException {
        String archivePath = cfgManager.getArchiveConfiguration().getPath();
        final File archive = new File(archivePath);
        if (!archive.exists()) {
            throw new DataStoreLocalArchiveNotExistingException(
                    "Local archive \"" + archivePath + "\" does not exist.");
        }

        LOGGER.info("Looking for new product in archive \"" + archivePath + "\".");

        final List<DrbCortexItemClass> supported = scannerFactory.getScannerSupport();

        Scanner scanner = scannerFactory.getScanner(archivePath);
        scanner.setSupportedClasses(supported);
        AsynchronousLinkedList<URLExt> list = scanner.getScanList();
        final Scanner s = scanner;

        list.addListener(new Listener<URLExt>() {
            @Override
            public void addedElement(final Event<URLExt> e) {
                try {
                    URL url = e.getElement().getUrl();
                    if (getProductByOrigin(url.toString()) != null || productDao.exists(url)) {
                        throw new DataStoreAlreadyExistException("Already in database");
                    }

                    LOGGER.info("Adding product \"" + url + "\".");
                    User owner = userDao.getRootUser();
                    Product p = addProduct(url, owner, null);
                    processProduct(p, owner, null, s, null);
                } catch (DataStoreAlreadyExistException excp) {
                    LOGGER.info("Product already in database : \"" + e.getElement().getUrl().toString() + "\".");
                } catch (DataStoreException excp) {
                    LOGGER.error("Cannot add product \"" + e.getElement().toString() + "\"", excp);
                }
            }

            @Override
            public void removedElement(Event<URLExt> e) {
            }
        });
        return scanner.scan();
    }

    /**
     * Remove unprocessed products
     */
    @Transactional(readOnly = false, propagation = Propagation.REQUIRED)
    @CacheEvict(value = { "product_count", "product", "products" }, allEntries = true)
    public void removeUnprocessed() {
        long start = System.currentTimeMillis();

        Iterator<Product> products = getUnprocessedProducts();
        while (products.hasNext()) {
            products.next();
            products.remove();
        }

        LOGGER.debug("Cleanup incomplete processed products in " + (System.currentTimeMillis() - start) + "ms");
    }

    @Transactional(readOnly = true)
    public Iterator<Product> getUnprocessedProducts() {
        return productDao.getUnprocessedProducts();
    }

    @Transactional(readOnly = true)
    public Product getProductByOrigin(final String origin) {
        DetachedCriteria criteria = DetachedCriteria.forClass(Product.class);
        criteria.add(Restrictions.eq("origin", origin));
        return productDao.uniqueResult(criteria);
    }

}