/* * 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.User; import fr.gael.dhus.datastore.processing.ProcessingUtils; import fr.gael.dhus.network.RegulatedInputStream; 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.InvalidTargetException; import fr.gael.dhus.olingo.v1.Model; import fr.gael.dhus.olingo.v1.MediaResponseBuilder; import fr.gael.dhus.olingo.v1.entityset.NodeEntitySet; import fr.gael.dhus.olingo.v1.map.impl.NodesMap; import fr.gael.dhus.util.DownloadActionRecordListener; import fr.gael.dhus.util.DownloadStreamCloserListener; import fr.gael.drb.DrbAttribute; import fr.gael.drb.DrbAttributeList; import fr.gael.drb.DrbFactory; import fr.gael.drb.DrbNode; import fr.gael.drb.impl.DrbNodeImpl; import fr.gael.drb.impl.spi.DrbNodeSpi; import fr.gael.drb.impl.xml.XmlFactory; import fr.gael.drb.value.Value; import fr.gael.drbx.cortex.DrbCortexModel; import java.io.BufferedInputStream; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.Closeable; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; 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; /** * The OData representation of a DRB Node. */ public class Node extends Item implements Closeable { private static final Logger LOGGER = LogManager.getLogger(Node.class); private final long ONE_YEAR_MS = (long)365.25*24*60*60*1000; protected DrbNode drbNode; private String path; private String contentType; private Long contentLength; private fr.gael.dhus.olingo.v1.entity.Class itemClass; private Map<String, Node> nodes; private Object value; private Map<String, Attribute> attributes; private Integer childrenNumber; public Node (String path) { super (getNodeId (path)); this.path=path; } public Node (DrbNode node) { super (getNodeId (node)); if (node == null) throw new NullPointerException ("Passed node cannot be null."); this.drbNode = node; } public void changeId (String id) { super.id = id; } private void initNode () { if (this.drbNode == null) { if (path == null) { throw new NullPointerException ("Node path cannot be null."); } this.drbNode = DrbFactory.openURI (path); LOGGER.debug("Initialized node : " + path); } if (this.drbNode==null) throw new NullPointerException ("Node cannot be null"); } @Override public String getName () { initNode (); return drbNode.getName (); } @Override public String getContentType () { initNode (); if (contentType == null) { try { DrbCortexModel model = DrbCortexModel.getDefaultModel (); contentType = model.getClassOf (drbNode).getLabel (); } catch (Exception e) { contentType = "Item"; } } return contentType; } @Override public Long getContentLength () { initNode (); if (contentLength == null) { contentLength = -1L; if (hasStream ()) { InputStream stream = getStream (); if (stream instanceof FileInputStream) { try { contentLength = ((FileInputStream) stream).getChannel ().size (); } catch (IOException e) { // Error while accessing file size: using -1L } } // Still not initialized ? if (contentLength == -1) { DrbAttribute attr = this.drbNode.getAttribute ("size"); if (attr != null) { try { contentLength = Long.decode (attr.getValue ().toString ()); } catch (NumberFormatException nfe) { // Error in attribute... } } } } else { contentLength = 0L; } } return contentLength; } @Override public Object navigate(NavigationSegment ns) throws ODataException { Object res; if (ns.getEntitySet().getName().equals(Model.NODE.getName())) { res = getNodes(); if (!ns.getKeyPredicates().isEmpty()) { res = ((NodesMap)res).get( ns.getKeyPredicates().get(0).getLiteral()); } } else if (ns.getEntitySet().getName().equals(Model.ATTRIBUTE.getName())) { res = getAttributes(); if (!ns.getKeyPredicates().isEmpty()) { res = Map.class.cast(res).get( ns.getKeyPredicates().get(0).getLiteral()); } } else if (ns.getEntitySet().getName().equals(Model.CLASS.getName())) { res = getItemClass(); } else { throw new InvalidTargetException(this.getClass().getSimpleName(), ns.getEntitySet().getName()); } return res; } public Integer getChildrenNumber () { initNode (); if (childrenNumber == null) { childrenNumber = drbNode.getChildrenCount (); } return childrenNumber; } public Object getValue () { initNode (); if (value == null) { Value val = drbNode.getValue (); String s_value = null; if (val != null) s_value = cleanInvalidXmlChars (val.toString (), ""); value = s_value; } return value; } public Map<String, Node> getNodes () { initNode (); if (nodes == null) { if (drbNode.hasChild ()) { nodes = new NodesMap (drbNode); } else { nodes = Collections.emptyMap (); } } return nodes; } public Map<String, Attribute> getAttributes () { initNode (); if (attributes == null) { DrbAttributeList attrs = drbNode.getAttributes (); Map<String, Attribute> attributes = new HashMap<String, Attribute> (); if ( (attrs != null) && (attrs.getLength () > 0)) { for (int index = 0; index < attrs.getLength (); index++) { DrbAttribute attr = attrs.item (index); String value = (attr.getValue () == null) ? "" : attr.getValue () .toString (); Attribute attribute = new Attribute(attr.getName(), value, null); attributes.put (attr.getName (), attribute); } } this.attributes = Collections.unmodifiableMap (new HashMap<String, Attribute> ( attributes)); } return attributes; } /** * Retrieve the Class from this Node entity. * @return the Class entity. * @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() { initNode (); if(this.itemClass==null) { try { itemClass = new fr.gael.dhus.olingo.v1.entity.Class( ProcessingUtils.getItemClassUri( ProcessingUtils.getClassFromNode(drbNode))); } catch(Exception e) { //throw new UnsupportedOperationException("Cannot find Drb model.",e); // Item class not found: use drb root item URI. itemClass = new fr.gael.dhus.olingo.v1.entity.Class( "http://www.gael.fr/drb#item"); } } return this.itemClass; } /** * Calls the superclass entity response not aggregated to this response. * @param root_url * @return the item class response. */ protected Map<String, Object> itemToEntityResponse (String root_url) { return super.toEntityResponse (root_url); } @Override public Map<String, Object> toEntityResponse (String root_url) { Map<String, Object> res = itemToEntityResponse (root_url); initNode (); res.put (NodeEntitySet.CHILDREN_NUMBER, getChildrenNumber ()); res.put (NodeEntitySet.VALUE, getValue ()); res.put (NodeEntitySet.PATH, drbNode); return res; } @Override public Object getProperty (String prop_name) throws ODataException { initNode (); if (prop_name.equals (NodeEntitySet.CHILDREN_NUMBER)) return getChildrenNumber (); if (prop_name.equals (NodeEntitySet.VALUE)) return getValue (); return super.getProperty (prop_name); } @Override public ODataResponse getEntityMedia (ODataSingleProcessor processor) throws ODataException { initNode (); if (hasStream ()) { try { User u = Security.getCurrentUser(); String user_name = (u == null ? null : u.getUsername ()); InputStream is = new BufferedInputStream (getStream()); RegulatedInputStream.Builder builder = new RegulatedInputStream.Builder (is, TrafficDirection.OUTBOUND); builder.userName (user_name); CopyStreamAdapter adapter = new CopyStreamAdapter (); CopyStreamListener recorder = new DownloadActionRecordListener ( this.getId (), this.getName (), u); CopyStreamListener closer = new DownloadStreamCloserListener (is); adapter.addCopyStreamListener (recorder); adapter.addCopyStreamListener (closer); builder.copyStreamListener (adapter); if (getContentLength ()>0) builder.streamSize(getContentLength()); is = builder.build(); String etag = getName () + "-" + getContentLength (); // A priori Node never change, so the lastModified should be as // far as possible than today. long last_modified = System.currentTimeMillis () - ONE_YEAR_MS; // If node is not a data file, it cannot be downloaded and set to -1 // As a stream exists, this control is probably obsolete. long content_length = getContentLength ()==0?-1:getContentLength (); return MediaResponseBuilder.prepareMediaResponse(etag, getName(), getContentType (), last_modified, content_length, processor.getContext (), is); } catch (Exception e) { throw new ODataException ( "An exception occured while creating the stream for node " + getName(), e); } } else { throw new ODataException ("No stream for node " + getName ()); } } public boolean hasStream () { initNode (); if (drbNode instanceof DrbNodeSpi) { return ((DrbNodeSpi) drbNode).hasImpl (InputStream.class); } return false; } public InputStream getStream () { initNode (); if (drbNode instanceof DrbNodeSpi) { return (InputStream) ((DrbNodeSpi) drbNode) .getImpl (InputStream.class); } return null; } public InputStream toXML () { initNode (); ByteArrayOutputStream out = new ByteArrayOutputStream (); XmlFactory.writeXML (drbNode, out); return new ByteArrayInputStream (out.toByteArray ()); } /** * Computes the unique identifier within this parent node context. The * retrieved identifier is computed from the XPath that includes the name of * this node, and the occurrence of this node within the parent if any. * * @param node this node to compute the id. * @return the unique identifier within this node. */ public static String getNodeId (String path) { String id = path; if (id.endsWith ("/")) id = id.substring (0, id.length () - 1); if (id.contains ("/")) { id = id.substring (id.lastIndexOf ("/") + 1); } if ( (id == null) || "".equals (id)) id = (new File (path)).getName (); return id; } public static String getNodeId (DrbNode node) { return getNodeId (node.getPath ()); } /** * From xml spec valid chars:<br> * #x9 | #xA | #xD | [#x20-#xD7FF] | [#xE000-#xFFFD] | [#x10000-#x10FFFF]<br> * any Unicode character, excluding the surrogate blocks, FFFE, and FFFF.<br> * * @param text The String to clean * @param replacement The string to be substituted for each match * @return The resulting String */ public static String cleanInvalidXmlChars (String text, String replacement) { String re = "[^\\x09\\x0A\\x0D\\x20-\\xD7FF\\xE000-\\xFFFD\\x10000-x10FFFF]"; return text.replaceAll (re, replacement); } @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() { // Node inherits from Item List<String> res = new ArrayList<>(super.getExpandableNavLinkNames()); 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 "Attributes": return Expander.mapToData(getAttributes(), self_url); case "Nodes": return Expander.mapToData(getNodes(), self_url); default: return super.expand(navlink_name, self_url); } } }