/* * Data Hub Service (DHuS) - For Space data distribution. * Copyright (C) 2013,2014,2015 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.olingo.v1.entity; import fr.gael.dhus.database.object.MetadataIndex; import fr.gael.dhus.database.object.Role; import fr.gael.dhus.database.object.User; import fr.gael.dhus.datastore.processing.ProcessingUtils; import fr.gael.dhus.network.RegulatedInputStream; import fr.gael.dhus.network.RegulationException; import fr.gael.dhus.network.TrafficDirection; import fr.gael.dhus.olingo.Security; import fr.gael.dhus.olingo.v1.Expander; import fr.gael.dhus.olingo.v1.ExpectedException.InvalidKeyException; import fr.gael.dhus.olingo.v1.ExpectedException.InvalidMediaException; import fr.gael.dhus.olingo.v1.ExpectedException.InvalidTargetException; import fr.gael.dhus.olingo.v1.ExpectedException.MediaRegulationException; import fr.gael.dhus.olingo.v1.ExpectedException.NotAllowedException; import fr.gael.dhus.olingo.v1.MediaResponseBuilder; import fr.gael.dhus.olingo.v1.Model; import fr.gael.dhus.olingo.v1.ServiceFactory; import fr.gael.dhus.olingo.v1.entityset.NodeEntitySet; import fr.gael.dhus.olingo.v1.entityset.ProductEntitySet; import fr.gael.dhus.service.EvictionService; import fr.gael.dhus.service.ProductService; import fr.gael.dhus.spring.context.ApplicationContextProvider; import fr.gael.dhus.system.config.ConfigurationManager; import fr.gael.dhus.util.DownloadActionRecordListener; import fr.gael.dhus.util.DownloadStreamCloserListener; import fr.gael.dhus.util.MetalinkBuilder; import fr.gael.drb.impl.DrbNodeImpl; import java.io.BufferedInputStream; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import javax.xml.transform.TransformerException; import org.apache.commons.net.io.CopyStreamAdapter; import org.apache.commons.net.io.CopyStreamListener; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.olingo.odata2.api.exception.ODataException; import org.apache.olingo.odata2.api.processor.ODataResponse; import org.apache.olingo.odata2.api.processor.ODataSingleProcessor; import org.apache.olingo.odata2.api.uri.NavigationSegment; /** * A OData representation of a DHuS Product. */ public class Product extends Node implements Closeable { private static final Logger LOGGER = LogManager.getLogger(Product.class); private static final EvictionService EVICTION_SERVICE = ApplicationContextProvider.getBean (EvictionService.class); private static final ProductService PRODUCT_SERVICE = ApplicationContextProvider.getBean (ProductService.class); /** To get the path of the incoming directory, to make localPaths. */ private static final ConfigurationManager CONFIG_MGR = ApplicationContextProvider.getBean (ConfigurationManager.class); protected final fr.gael.dhus.database.object.Product product; protected Map<String, Node> nodes; protected Map<String, Attribute> attributes; private Map<String, Product> products; public static void delete (String uuid) throws ODataException { if (Security.currentUserHasRole(Role.DATA_MANAGER)) { fr.gael.dhus.database.object.Product p = PRODUCT_SERVICE.systemGetProduct (uuid); if (p == null) { throw new InvalidKeyException(uuid, Product.class.getSimpleName()); } PRODUCT_SERVICE.systemDeleteProduct (p.getId ()); } else { throw new NotAllowedException(); } } public Product (fr.gael.dhus.database.object.Product product) { super (product.getPath ().toString ()); this.product = product; } /** * Retrieve the Class from this product entity. * * @return the DrbCortex class name. * @throws UnsupportedOperationException if the model cannot be computed. * @throws NullPointerException if this product does not related any class. */ @Override public fr.gael.dhus.olingo.v1.entity.Class getItemClass () { // Case of ingestion performed before DHuS 0.4.4 if (product.getItemClass () == null) { try { return new fr.gael.dhus.olingo.v1.entity.Class ( ProcessingUtils.getItemClassUri (ProcessingUtils .getClassFromProduct (this.product))); } catch (Exception e) { throw new UnsupportedOperationException ("Cannot find product.", e); } } return new fr.gael.dhus.olingo.v1.entity.Class (product.getItemClass ()); } @Override public String getId () { return product.getUuid (); } @Override public String getName () { return product.getIdentifier (); } @Override public String getContentType () { return "application/octet-stream"; } @Override public Long getContentLength () { return product.getDownloadableSize(); } @Override public Integer getChildrenNumber () { int number = 0; if (this.product != null) { if (this.product.getQuicklookFlag ()) number++; if (this.product.getThumbnailFlag ()) number++; } return number; } @Override public Object getValue () { return null; } public Date getIngestionDate () { return product.getIngestionDate (); } public Date getEvictionDate () { // dynamic date return EVICTION_SERVICE.getEvictionDate (product.getId ()); } public Date getCreationDate () { return product.getCreated (); } public String getGeometry () { return product.getFootPrint (); } public Date getContentStart () { return product.getContentStart (); } public Date getContentEnd () { return product.getContentEnd (); } public boolean hasChecksum () { return ! (product.getDownload ().getChecksums ().isEmpty ()); } public String getChecksumAlgorithm () { if ( ! (hasChecksum ())) return null; Map<String, String> checksum = product.getDownload ().getChecksums (); String algorithm = "MD5"; if (checksum.get (algorithm) != null) return algorithm; return checksum.keySet ().iterator ().next (); } public String getChecksumValue () { if ( ! (hasChecksum ())) return null; return product.getDownload ().getChecksums () .get (getChecksumAlgorithm ()); } /** * This product requires system controls (statistics/quotas) * * @return true is control is required, false otherwise. */ public boolean requiresControl () { // TODO This method shall be replaced by RABAC mechanism return true; } // Getters public Map<String, Product> getProducts () { if (this.products == null) { Map<String, Product> products = new LinkedHashMap<String, Product> (); if (this.product.getQuicklookFlag ()) { products.put ("Quicklook", new QuicklookProduct (product)); } if (this.product.getThumbnailFlag ()) { products.put ("Thumbnail", new ThumbnailProduct (product)); } this.products = products; } return products; } @Override public Map<String, Node> getNodes () { if (this.nodes == null) { this.nodes = new LinkedHashMap<String, Node> (); this.drbNode = ProcessingUtils.getNodeFromPath (product.getPath ().getPath ()); if (this.drbNode == null) throw new NullPointerException ("Cannot compute DRB node from " + product.getPath ().getPath ()); this.nodes.put (this.drbNode.getName (), new Node (this.drbNode)); } return this.nodes; } @Override public Map<String, Attribute> getAttributes () { if (this.attributes == null) { this.attributes = new LinkedHashMap<String, Attribute> (); for (MetadataIndex index : PRODUCT_SERVICE.getIndexes (this.product.getId ())) { Attribute attr= new Attribute(index.getName(), index.getValue(), index.getCategory()); this.attributes.put(attr.getName(), attr); } } return this.attributes; } /** * Returns the absolute local path to this product. * * @return path to this product. */ public String getDownloadablePath () { return product.getDownload ().getPath (); } public InputStream getInputStream () throws IOException { return new FileInputStream (product.getDownload ().getPath ()); } /** * Returns the Metalink4 document for this Product. * @param root_url required to generate links in the Metalink document. * @return Metalink document as String, may return {@code null}. */ public String getMetalink(String root_url) { String url = String.format("%s%s('%s')/$value", root_url, Model.PRODUCT.getName(), getId()); MetalinkBuilder mb = new MetalinkBuilder(); mb.addFile(this.getName() + ".zip") .addUrl(url, null, 0) .setSize(this.getContentLength()) .setHash(this.getChecksumAlgorithm(), this.getChecksumValue()); try { return mb.buildToString(false); } catch (TransformerException ex) { LOGGER.error("Metalink4 XML doc to String error", ex); } return null; } @Override public Map<String, Object> toEntityResponse (String root_url) { // superclass node response is not required. Only Item response is // necessary. Map<String, Object> res = super.itemToEntityResponse (root_url); res.put (NodeEntitySet.CHILDREN_NUMBER, getChildrenNumber ()); LinkedHashMap<String, Date> dates = new LinkedHashMap<String, Date> (); dates.put(Model.TIME_RANGE_START, getContentStart()); dates.put(Model.TIME_RANGE_END, getContentEnd()); res.put (ProductEntitySet.CONTENT_DATE, dates); HashMap<String, String> checksum = new LinkedHashMap<String, String> (); checksum.put(Model.ALGORITHM, getChecksumAlgorithm()); checksum.put(Model.VALUE, getChecksumValue()); res.put (ProductEntitySet.CHECKSUM, checksum); res.put (ProductEntitySet.INGESTION_DATE, getIngestionDate ()); res.put (ProductEntitySet.CREATION_DATE, getCreationDate ()); res.put (ProductEntitySet.EVICTION_DATE, getEvictionDate ()); res.put (ProductEntitySet.CONTENT_GEOMETRY, getGeometry ()); Path incoming_path = Paths.get (CONFIG_MGR.getArchiveConfiguration () .getIncomingConfiguration ().getPath ()); String prod_path = this.getDownloadablePath (); if (prod_path != null) // Can happen with not yet ingested products { Path prod_path_path = Paths.get (prod_path); if (prod_path_path.startsWith (incoming_path)) { prod_path = incoming_path.relativize (prod_path_path).toString (); } else { prod_path = null; } } else { prod_path = null; } res.put (ProductEntitySet.LOCAL_PATH, prod_path); res.put (ProductEntitySet.METALINK, getMetalink(root_url)); return res; } @Override public Object getProperty (String prop_name) throws ODataException { switch(prop_name) { case ProductEntitySet.CREATION_DATE: return getCreationDate(); case ProductEntitySet.INGESTION_DATE: return getIngestionDate(); case ProductEntitySet.EVICTION_DATE: return getEvictionDate(); case ProductEntitySet.CONTENT_GEOMETRY: return getGeometry(); case ProductEntitySet.METALINK: return getMetalink(ServiceFactory.ROOT_URL); default: return super.getProperty (prop_name); } } @Override public Map<String, Object> getComplexProperty (String prop_name) throws ODataException { if (prop_name.equals (ProductEntitySet.CONTENT_DATE)) { Map<String, Object> values = new HashMap<String, Object> (); values.put(Model.TIME_RANGE_START, getContentStart()); values.put(Model.TIME_RANGE_END, getContentEnd()); return values; } if (prop_name.equals (ProductEntitySet.CHECKSUM)) { Map<String, Object> values = new HashMap<String, Object> (); values.put(Model.ALGORITHM, getChecksumAlgorithm()); values.put(Model.VALUE, getChecksumValue()); return values; } throw new ODataException ("Complex property '" + prop_name + "' not found."); } @Override public ODataResponse getEntityMedia(ODataSingleProcessor processor) throws ODataException { ODataResponse rsp = null; try { InputStream is = new BufferedInputStream(getInputStream()); if (requiresControl()) { User u = Security.getCurrentUser(); String user_name = (u == null ? null : u.getUsername()); CopyStreamAdapter adapter = new CopyStreamAdapter(); CopyStreamListener recorder = new DownloadActionRecordListener(product.getUuid(), product.getIdentifier(), u); CopyStreamListener closer = new DownloadStreamCloserListener(is); adapter.addCopyStreamListener(recorder); adapter.addCopyStreamListener(closer); RegulatedInputStream.Builder builder = new RegulatedInputStream.Builder(is, TrafficDirection.OUTBOUND); builder.userName(user_name); builder.copyStreamListener(adapter); builder.streamSize(getContentLength()); is = builder.build(); } // Computes ETag String etag = getChecksumValue(); if (etag == null) { etag = getId(); } String filename = new File(getDownloadablePath()).getName(); // Prepare the HTTP header for stream transfer. rsp = MediaResponseBuilder.prepareMediaResponse( etag, filename, getContentType(), getCreationDate().getTime(), getContentLength(), processor.getContext(), is); } // RegulationException must be handled separately as they are // user generated errors and not internal problems catch (RegulationException e) { throw new MediaRegulationException(e.getMessage()); } catch (IOException e) { throw new InvalidMediaException(e.getMessage()); } return rsp; } @Override public Object navigate(NavigationSegment ns) throws ODataException { Object res; if (ns.getEntitySet().getName().equals(Model.NODE.getName())) { res = getNodes(); } else if (ns.getEntitySet().getName().equals(Model.ATTRIBUTE.getName())) { res = getAttributes(); } else if (ns.getEntitySet().getName().equals(Model.CLASS.getName())) { res = getItemClass(); } else if (ns.getEntitySet().getName().equals(Model.PRODUCT.getName())) { res = getProducts(); } else { throw new InvalidTargetException(this.getClass().getSimpleName(), ns.getEntitySet().getName()); } if (!ns.getKeyPredicates().isEmpty()) { res = Map.class.cast(res).get( ns.getKeyPredicates().get(0).getLiteral()); } return res; } @Override public void close () throws IOException { if (this.drbNode == null) return; if (this.drbNode instanceof DrbNodeImpl) { DrbNodeImpl.class.cast(this.drbNode).close(true); } } @Override public List<String> getExpandableNavLinkNames() { // Product inherits from Node List<String> res = new ArrayList<>(super.getExpandableNavLinkNames()); res.add("Products"); res.add("Class"); res.add("Attributes"); res.add("Nodes"); return res; } @Override public List<Map<String, Object>> expand(String navlink_name, String self_url) { switch(navlink_name) { case "Products": return Expander.mapToData(getProducts(), self_url); case "Class": return Expander.entityToData(getItemClass(), self_url); default: return super.expand(navlink_name, self_url); } } }