myexperiment-hackers
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[myexperiment-hackers] [3559] branches/packs: partial implementation of


From: noreply
Subject: [myexperiment-hackers] [3559] branches/packs: partial implementation of the RO API
Date: Wed, 22 May 2013 13:45:00 +0000 (UTC)

Revision
3559
Author
dgc
Date
2013-05-22 13:45:00 +0000 (Wed, 22 May 2013)

Log Message

partial implementation of the RO API

Modified Paths

Added Paths

Diff

Added: branches/packs/app/controllers/research_objects_controller.rb (0 => 3559)


--- branches/packs/app/controllers/research_objects_controller.rb	                        (rev 0)
+++ branches/packs/app/controllers/research_objects_controller.rb	2013-05-22 13:45:00 UTC (rev 3559)
@@ -0,0 +1,81 @@
+require 'securerandom'
+
+class ResearchObjectsController < ActionController::Base
+
+  # GET /rodl
+  def index
+
+    uri_list = ""
+
+    ResearchObject.all.each do |ro|
+      uri_list << "#{research_object_url(ro.slug)}/\n"
+    end
+
+    send_data(uri_list, :type => 'text/uri-list')
+  end
+
+  # GET /rodl/:id
+  def show
+
+    slug = params[:id]
+    slug = slug[0..-2] if slug.ends_with?("/")
+
+    ro = ResearchObject.find_by_slug_and_version(slug, nil)
+
+    if (ro)
+      redirect_to research_object_url(slug) + "/" + ResearchObject::MANIFEST_PATH, :status => 303
+    else
+      render :text => "Research Object not found", :status => 404
+    end
+  end
+
+  # POST /rodl
+  def create
+    
+    current_user = User.find(1) # FIXME - hardcoded
+    
+    slug = request.headers["Slug"]
+    
+    # Remove trailing slash if given.
+
+    slug = slug[0..-2] if slug.ends_with?("/")
+
+    # If a research object exists with the slug then respond with 409 Conflict.
+
+    if ResearchObject.find_by_slug_and_version(slug, nil)
+      render :nothing => true, :status => 409
+      return
+    end
+
+    # Create the research object with a blank manifest.  The blank manifest is
+    # created so that when the manifest is aggregated it contains the
+    # description of the manifest.
+
+    ro_uri = research_object_url(slug) + "/"
+
+    ro = ResearchObject.create(:slug => slug, :user => current_user)
+
+    response.headers["Location"] = ro_uri
+
+    send_data(ro.manifest_resource.data, :type => "application/rdf+xml", :filename => ResearchObject::MANIFEST_PATH, :status => 201)
+  end
+
+  # DELETE /rodl/:id
+  def destroy
+
+    ro = ResearchObject.find_by_slug_and_version(params[:id], nil)
+    
+    if ro
+      ro.destroy
+      render :nothing => true, :status => 204
+    else
+      render :text => "Research Object not found", :status => 404
+    end
+  end
+
+  # PUT /rodl/:id
+  def update
+  end
+
+end
+

Added: branches/packs/app/controllers/resources_controller.rb (0 => 3559)


--- branches/packs/app/controllers/resources_controller.rb	                        (rev 0)
+++ branches/packs/app/controllers/resources_controller.rb	2013-05-22 13:45:00 UTC (rev 3559)
@@ -0,0 +1,150 @@
+require 'securerandom'
+
+class ResourcesController < ActionController::Base
+
+  include ResearchObjectsHelper
+
+  before_filter :dump_request_to_log
+  after_filter :dump_response_to_log
+
+  def dump_to_log(thing, type)
+    # Dump headers
+
+    logger.info "---- #{type} headers ----------------------------------"
+    thing.headers.each do |header, value|
+      if header.starts_with?("HTTP_")
+          logger.info sprintf("%s: %s\n", header.sub(/^HTTP_/, "").downcase, value)
+      end
+    end
+
+    logger.info "Content-Type: " + thing.content_type.to_s
+    if thing.body
+      logger.info "---- #{type} body  ------------------------------------"
+      if thing.body.kind_of?(String)
+        logger.info thing.body
+      else
+        logger.info thing.body.read
+        thing.body.rewind
+      end
+    end
+    logger.info "---- #{type} end  ------------------------------------"
+
+  end
+
+  def dump_request_to_log
+    dump_to_log(request, 'Request')
+  end
+
+  def dump_response_to_log
+    dump_to_log(response, 'Response')
+  end
+
+  def show
+
+    ro = ResearchObject.find_by_slug_and_version(params[:research_object_id], nil)
+
+    unless ro
+      render :text => "Research Object not found", :status => :not_found
+      return
+    end
+
+    resource = ro.resources.find_by_path(params[:id])
+
+    unless resource
+      render :text => "Resource not found", :status => :not_found
+      return
+    end
+
+    send_data(resource.data, :type => resource.content_type)
+  end
+
+  def post
+
+    current_user = User.find(1) # FIXME - hardcoded
+
+    research_object = ResearchObject.find_by_slug_and_version(params[:research_object_id], nil)
+
+    unless research_object
+      render :text => "Research Object not found", :status => :not_found
+      return
+    end
+
+    slug = request.headers["Slug"].gsub(" ", "%20") if request.headers["Slug"]
+
+    status, reason, location, links, filename, changes = research_object.new_or_update_resource(
+        :slug         => slug,
+        :path         => params[:path],
+        :content_type => request.content_type.to_s,
+        :user_uri     => user_url(current_user),
+        :data         ="" request.body.read,
+        :links        => parse_links(request.headers))
+
+    research_object.update_manifest! if status == :created
+
+    response.headers["Location"] = location      if location.kind_of?(String)
+    response.headers["Location"] = location.to_s if location.kind_of?(RDF::URI)
+
+    if links.length > 0
+      response.headers['Link'] = links.map do |link|
+        "<#{link[:link].kind_of?(RDF::URI) ? link[:link].to_s : link[:link]}>; " +
+        "rel=\"#{link[:rel].kind_of?(RDF::URI) ? link[:rel].to_s : link[:rel]}\""
+      end
+    end
+
+    if status == :created
+
+      graph = RDF::Graph.new
+
+      changes.each do |change|
+        graph << research_object.resources.find_by_path(change).description
+      end
+
+      body = pretty_rdf_xml(RDF::Writer.for(:rdfxml).buffer { |writer| writer << graph } )
+
+      send_data body, :type => 'application/rdf+xml', :filename => filename, :status => :created
+    else
+      render :status => status, :text => reason
+    end
+  end
+
+  def delete
+
+    current_user = User.find(1) # FIXME - hardcoded
+
+    ro = ResearchObject.find_by_slug_and_version(params[:research_object_id], nil)
+
+    unless ro
+      render :text => "Research Object not found", :status => :not_found
+      return
+    end
+
+    path = params[:id]
+
+    if path == ResearchObject::MANIFEST_PATH
+      render :text => "Cannot delete the manifest", :status => :forbidden
+      return
+    end
+
+    resource = ro.resources.find_by_path(path)
+
+    if resource.nil?
+      render :text => "Resource not found", :status => :not_found
+      return
+    end
+
+    # Delete the proxy if one exists for this resource.
+
+    if resource.content_type == 'application/vnd.wf4ever.proxy'
+      proxy = resource
+      resource = Resource.find_by_path(resource.proxy_for_path)
+    else
+      proxy = Resource.find_by_proxy_for_path(resource.path)
+    end
+
+    proxy.destroy    if proxy
+    resource.destroy if resource
+
+    render :nothing => true, :status => :no_content    
+  end
+
+end

Added: branches/packs/app/helpers/research_objects_helper.rb (0 => 3559)


--- branches/packs/app/helpers/research_objects_helper.rb	                        (rev 0)
+++ branches/packs/app/helpers/research_objects_helper.rb	2013-05-22 13:45:00 UTC (rev 3559)
@@ -0,0 +1,172 @@
+
+require 'xml/libxml'
+
+module ResearchObjectsHelper
+
+  NAMESPACES = {
+      "http://purl.org/dc/terms/"              => "dct",
+      "http://www.openarchives.org/ore/terms/" => "ore",
+      "http://purl.org/ao/"                    => "ao",
+      "http://purl.org/wf4ever/ro#"            => "ro"
+  }
+
+  def pretty_rdf_xml(text)
+
+    descriptions = { }
+
+    doc = LibXML::XML::Parser.string(text).parse
+
+    # Merge descriptions where the subject is the same.
+
+    doc.root.find("/rdf:RDF/rdf:Description").each do |description|
+
+      resource = description.attributes["about"]
+
+      if descriptions[resource]
+
+        if descriptions[resource].children.last.to_s == "\n  "
+          descriptions[resource].children.last.remove!
+          descriptions[resource].children << "\n"
+        end
+
+        description.each do |object|
+          if object.element?
+            descriptions[resource] << "\n    "
+            descriptions[resource] << object
+            descriptions[resource] << "\n  "
+          end
+        end
+
+        description.prev.remove!
+        description.remove!
+      else
+        descriptions[resource] = description
+
+        description.prev = XML::Node.new_text("\n  ")
+      end
+    end
+
+    doc.root << XML::Node.new_text("\n")
+
+    # Adjust namespaces to human readable names
+
+    namespaces = { "rdf" => "http://www.w3.org/1999/02/22-rdf-syntax-ns#" }
+
+    doc.root.find("/rdf:RDF/rdf:Description/*").each do |object|
+
+      next unless object.element?
+      next unless object.namespaces.namespace.prefix == "ns0"
+
+      href = ""
+      prefix = NAMESPACES[href]
+
+      namespaces[prefix] = href unless namespaces[prefix]
+
+      if prefix
+        el = XML::Node.new(prefix + ":" + object.name)
+
+        # Copy attributes
+
+        object.attributes.each do |attr|
+          if attr.ns?
+            el.attributes[attr.ns.prefix + ":" + attr.name] = attr.value
+          else
+            el.attributes[attr.name] = attr.value
+          end
+        end
+
+        # Move children
+
+        object.children.each do |child|
+          el << child.remove!
+        end
+
+        object.next = el
+        object.remove!
+      end
+    end
+
+    # Add namespaces
+
+    namespaces.delete("rdf")
+
+    namespaces.each do |prefix, href|
+      next if prefix == "rdf"
+      LibXML::XML::Namespace.new(doc.root, prefix, href)
+    end
+
+    text = doc.to_s
+
+    text.gsub!(/" xmlns:/, "\"\n         xmlns:") # FIXME - do this without affecting the entire document
+
+    text
+  end
+
+  def relative_uri(uri, context)
+
+    uri     = uri.to_s
+    context = context.to_s
+
+    if (uri == context)
+      candidate = "."
+    elsif uri.starts_with?(context)
+      candidate = uri[context.length..-1]
+    end
+
+    return uri if candidate.nil?
+    return uri if URI(context).merge(candidate).to_s != uri
+
+    candidate
+  end
+
+  def parse_links(headers)
+
+    links = {}
+
+    link_headers = headers["Link"]
+
+    if link_headers
+      link_headers.split(",").each do |link|
+        matches = link.strip.match(/<([^>]*)>\s*;.*rel\s*=\s*"?([^;"]*)"?/)
+        if matches
+          links[matches[2]] ||= []
+          links[matches[2]] << matches[1]
+        end
+      end
+    end
+
+    links
+  end
+
+  def calculate_path(path, content_type, links = {})
+
+    return path if path
+
+    case content_type
+    when "application/vnd.wf4ever.proxy"
+      ".ro/proxies/#{SecureRandom.uuid}"
+    when "application/vnd.wf4ever.annotation"
+      ".ro/annotations/#{SecureRandom.uuid}"
+    when "application/vnd.wf4ever.folder"
+      ".ro/folders/#{SecureRandom.uuid}"
+    when "application/vnd.wf4ever.folderentry"
+      ".ro/entries/#{SecureRandom.uuid}"
+    else
+      SecureRandom.uuid
+    end
+  end
+
+  def load_graph(content)
+    graph = RDF::Graph.new # FIXME - this should support more than just rdf+xml
+    graph << RDF::Reader.for(:rdfxml).new(content) if content
+    graph
+  end
+
+  def create_rdf_xml(&blk)
+    graph = RDF::Graph.new
+    yield(graph)
+    pretty_rdf_xml(RDF::Writer.for(:rdfxml).buffer { |writer| writer << graph })
+  end
+
+end
+

Added: branches/packs/app/models/annotation_resource.rb (0 => 3559)


--- branches/packs/app/models/annotation_resource.rb	                        (rev 0)
+++ branches/packs/app/models/annotation_resource.rb	2013-05-22 13:45:00 UTC (rev 3559)
@@ -0,0 +1,5 @@
+
+class AnnotationResource < ActiveRecord::Base
+  belongs_to :annotation, :class_name => "Resource"
+end
+

Added: branches/packs/app/models/research_object.rb (0 => 3559)


--- branches/packs/app/models/research_object.rb	                        (rev 0)
+++ branches/packs/app/models/research_object.rb	2013-05-22 13:45:00 UTC (rev 3559)
@@ -0,0 +1,469 @@
+require 'rdf'
+require 'rdf/raptor'
+
+class ResearchObject < ActiveRecord::Base
+
+  MANIFEST_PATH = ".ro/manifest.rdf"
+
+  include ResearchObjectsHelper
+
+  after_create :create_manifest
+
+  belongs_to :user
+
+  has_many :resources, :dependent => :destroy
+
+  validates_presence_of :slug
+
+  def uri
+    Conf.base_uri + "/rodl/ROs/#{slug}/"
+  end
+
+  def creator_uri
+    Conf.base_uri + "/users/#{user_id}"
+  end
+
+  def description
+
+    ro_uri = RDF::URI(uri)
+
+    graph = RDF::Graph.new
+
+    graph << [ro_uri, RDF.type, RDF::URI("http://purl.org/wf4ever/ro#ResearchObject")]
+    graph << [ro_uri, RDF.type, RDF::URI("http://www.openarchives.org/ore/terms/Aggregation")]
+    graph << [ro_uri, RDF::URI("http://www.openarchives.org/ore/terms/isDescribedBy"), ro_uri + MANIFEST_PATH]
+    graph << [ro_uri, RDF::DC.created, created_at.to_datetime]
+    graph << [ro_uri, RDF::DC.creator, RDF::URI(creator_uri)]
+
+    graph << [RDF::URI(creator_uri), RDF.type, RDF::FOAF.Agent]
+
+    resources.each do |resource|
+
+      if resource.is_aggregated
+        graph << [ro_uri, RDF::URI("http://www.openarchives.org/ore/terms/aggregates"), RDF::URI(resource.resource_uri)]
+      end
+
+      graph << resource.description
+    end
+
+    graph
+  end
+
+  def update_manifest!
+
+    resources.reload
+
+    manifest_body = pretty_rdf_xml(RDF::Writer.for(:rdfxml).buffer { |writer| writer << description })
+
+    new_or_update_resource(
+        :slug         => MANIFEST_PATH,
+        :content_type => "application/rdf+xml",
+        :data         ="" manifest_body,
+        :force_write  => true) 
+  end
+
+  def manifest_resource
+    resources.find(:first, :conditions => { :path => MANIFEST_PATH })
+  end
+
+  def new_or_update_resource(opts = {})
+
+    changed_descriptions = []
+    links = []
+    location = nil
+
+    content_type  = opts[:content_type]
+    slug          = opts[:slug]
+    path          = opts[:slug]
+    user_uri      = opts[:user_uri]
+    data          = ""
+    request_links = opts[:links] || {}
+
+    if slug == ResearchObject::MANIFEST_PATH
+
+      path = calculate_path(slug, content_type, request_links)
+
+      return [:forbidden, "Cannot overwrite the manifest", nil, []] unless opts[:force_write]
+
+      resource = resources.find_by_path(path)
+      resource = resources.new(:path => path) unless resource
+
+      resource.content_blob.destroy if resource.content_blob
+
+      resource.data = "" # FIXME - to be removed
+      resource.content_blob = ContentBlob.new(:data ="" data)
+      resource.creator_uri = user_uri
+      resource.content_type = content_type
+      resource.name = path.split("/").last if path
+
+      resource.is_resource   = false
+      resource.is_aggregated = false
+      resource.is_proxy      = false
+      resource.is_annotation = false
+      resource.is_folder     = false
+
+
+      resource.save
+
+      changed_descriptions << resource.path
+
+    elsif content_type == "application/vnd.wf4ever.proxy"
+
+      path = calculate_path(slug, content_type, request_links)
+
+      # Obtain information to create the proxy.
+
+      graph = load_graph(data)
+
+      node           = graph.query([nil,  RDF.type,     ORE.proxy]).first_subject
+      proxy_for_uri  = graph.query([node, ORE.proxyFor, nil           ]).first_object
+
+      # Contruct the proxy.
+
+      ro_uri         = RDF::URI(uri)
+      proxy_uri      = ro_uri + path
+      proxy_in_uri   = ro_uri
+      proxy_for_path = relative_uri(proxy_for_uri, uri)
+      proxy_in_path  = relative_uri(proxy_in_uri, uri)
+
+      proxy_body = create_rdf_xml do |graph|
+        graph << [proxy_uri, RDF.type,     ORE.Proxy]
+        graph << [proxy_uri, ORE.proxyIn,  proxy_in_uri]
+        graph << [proxy_uri, ORE.proxyFor, proxy_for_uri]
+      end
+
+      proxy = resources.find_by_path(path)
+      proxy = resources.new(:path => path) unless proxy
+
+      proxy.content_blob.destroy if proxy.content_blob
+
+      proxy.is_proxy       = true
+      proxy.proxy_for_path = proxy_for_path
+      proxy.proxy_in_path  = proxy_in_path
+      proxy.data           = "" # FIXME - to be removed
+      proxy.content_blob   = ContentBlob.new(:data ="" proxy_body)
+      proxy.creator_uri    = user_uri
+      proxy.content_type   = content_type
+      proxy.name           = path.split("/").last
+
+      proxy.save
+
+      location = proxy.uri
+      links << { :link => proxy_for_uri, :rel => ORE.proxyFor }
+
+      changed_descriptions << proxy.path
+
+    elsif content_type == "application/vnd.wf4ever.annotation"
+
+      path = calculate_path(slug, content_type, request_links)
+
+      resource = resources.find_by_path(path)
+      resource = resources.new(:path => path) unless resource
+
+      resource.content_blob.destroy if resource.content_blob
+
+      resource.data = "" # FIXME - to be removed
+      resource.content_blob = ContentBlob.new(:data ="" data)
+      resource.creator_uri = user_uri
+      resource.content_type = content_type
+      resource.name = path.split("/").last if path
+
+      # Creation of an annotation stub directly
+
+      resource.is_resource   = false
+      resource.is_aggregated = true
+      resource.is_proxy      = false
+      resource.is_annotation = true
+      resource.is_folder     = false
+
+      graph = load_graph(resource.data)
+
+      aggregated_annotations = graph.query([nil, RDF.type, RDF::URI("http://purl.org/wf4ever/ro#AggregatedAnnotation")])
+
+      if aggregated_annotations.count != 1 # FIXME - add test case
+        return [:unprocessable_entity, "The annotation must contain exactly one aggregated annotation", nil, []]
+      end
+
+      aggregated_annotation = aggregated_annotations.first_subject
+
+      ao_body_statements = graph.query([aggregated_annotation, RDF::URI("http://purl.org/ao/body"), nil])
+
+      if ao_body_statements.count != 1 # FIXME - add test case
+        return [:unprocessable_entity, "Annotations must contain exactly one annotation body", nil, []]
+      end
+
+      ao_body_uri = ao_body_statements.first_object.to_s
+
+      resource.ao_body_path = relative_uri(ao_body_uri, uri)
+
+      annotated_resources_statements = graph.query([aggregated_annotation, RDF::URI("http://purl.org/ao/annotatesResource"), nil])
+
+      if annotated_resources_statements.count == 0 # FIXME - add test case
+        return [:unprocessable_entity, "Annotations must annotate one or more resources", nil, []]
+      end
+
+      annotated_resources_statements.each do |annotated_resource|
+        resource.annotation_resources.build(:resource_uri => annotated_resource.object.to_s)
+        links << { :link => annotated_resource.object.to_s, :rel => "http://purl.org/ao/annotatesResource" }
+      end
+
+      links << { :link => ao_body_uri, :rel => "http://purl.org/ao/body" }
+
+
+      resource.save
+
+      changed_descriptions << resource.path
+
+    elsif content_type == "application/vnd.wf4ever.folder"
+
+      path = calculate_path(slug, content_type, request_links)
+
+      resource = resources.find_by_path(path)
+      resource = resources.new(:path => path) unless resource
+
+      resource.content_blob.destroy if resource.content_blob
+
+      resource.data = "" # FIXME - to be removed
+      resource.content_blob = ContentBlob.new(:data ="" data)
+      resource.creator_uri = user_uri
+      resource.content_type = content_type
+      resource.name = path.split("/").last if path
+
+
+      resource.is_resource   = false
+      resource.is_aggregated = true
+      resource.is_proxy      = false
+      resource.is_annotation = false
+      resource.is_folder     = true
+      
+      # Create a resource map for this folder
+
+      resource_uri = resource.resource_uri.to_s
+
+      resource_map_path = ".ro/resource_maps/#{SecureRandom.uuid}"
+      resource_map_uri  = uri + resource_map_path
+
+      resource_map_graph = RDF::Graph.new
+      resource_map_graph << [RDF::URI(resource_map_uri), RDF.type, RDF::URI("http://www.openarchives.org/ore/terms/ResourceMap")]
+      resource_map_graph << [RDF::URI(resource_map_uri), RDF::URI("http://www.openarchives.org/ore/terms/describes"), RDF::URI(resource_uri)]
+
+      resource_map_body = pretty_rdf_xml(RDF::Writer.for(:rdfxml).buffer { |writer| writer << resource_map_graph } )
+
+      # FIXME - this should be a recursive call
+
+      resource_map_attributes = {
+        :data            ="" resource_map_body,
+        :content_blob    => ContentBlob.new(:data ="" resource_map_body),
+        :creator_uri     => user_uri,
+        :content_type    => 'application/vnd.wf4ever.folder',
+        :is_resource_map => true,
+        :path            => resource_map_path
+      }
+
+      resources.create(resource_map_attributes)
+
+      resource.resource_map_path = resource_map_path
+
+      links << { :link => resource_map_uri, :rel => "http://www.openarchives.org/ore/terms/isDescribedBy" }
+      links << { :link => resource_uri,     :rel => "http://www.openarchives.org/ore/terms/proxyFor"      }
+
+      changed_descriptions << resource_map_path
+
+
+      resource.save
+
+      changed_descriptions << resource.path
+
+    elsif content_type == "application/vnd.wf4ever.folderentry"
+
+      path = calculate_path(nil, 'application/vnd.wf4ever.folderentry')
+
+      # Obtain information to create the folder entry.
+
+      graph = load_graph(data)
+
+      node           = graph.query([nil,  RDF.type,     RO.FolderEntry]).first_subject
+      proxy_for_uri  = graph.query([node, ORE.proxyFor, nil           ]).first_object
+
+      proxy_for_path = relative_uri(proxy_for_uri, uri)
+      proxy_in_path  = opts[:path]
+      proxy_in_uri   = uri + proxy_in_path
+
+      # Create the folder entry
+
+      folder_entry = resources.new(:path => path)
+
+      folder_entry_uri = RDF::URI(folder_entry.uri)
+
+      folder_entry_body = create_rdf_xml do |graph|
+        graph << [folder_entry_uri, RDF.type, ORE.Proxy]
+        graph << [folder_entry_uri, RDF.type, RO.FolderEntry]
+        graph << [folder_entry_uri, ORE.proxyIn, RDF::URI(proxy_in_uri)]
+        graph << [folder_entry_uri, ORE.proxyFor, RDF::URI(proxy_for_uri)]
+      end
+
+      folder_entry.is_folder_entry = true
+      folder_entry.data = "" # FIXME - to be removed
+      folder_entry.content_blob    = ContentBlob.new(:data ="" folder_entry_body)
+      folder_entry.proxy_in_path   = proxy_in_path
+      folder_entry.proxy_for_path  = proxy_for_path
+      folder_entry.content_type    = content_type
+      folder_entry.creator_uri     = user_uri
+      folder_entry.name            = path.split("/").last if path
+
+      folder_entry.save
+
+      folder_entry.proxy_for.update_attribute(:aggregated_by_path, proxy_in_path)
+
+      location = folder_entry.uri
+
+      links << { :link => proxy_for_uri, :rel => "http://www.openarchives.org/ore/terms/proxyFor" }
+
+    elsif request_links["http://purl.org/ao/annotatesResource"]
+
+      path           = calculate_path(nil, content_type)
+      ro_uri         = RDF::URI(uri)
+      annotation_uri = ro_uri + path
+
+
+      # Create an annotation body using the provided graph
+
+      # Process ao:annotatesResource links by creating annotation stubs using the
+      # given resource as an ao:body.
+
+      ao_body_path = calculate_path(slug, content_type, request_links)
+
+      ao_body = resources.find_by_path(path)
+      ao_body = resources.new(:path => path) unless ao_body
+
+      ao_body.content_blob.destroy if ao_body.content_blob
+
+      ao_body.data          = "" # FIXME - to be removed
+      ao_body.content_blob  = ContentBlob.new(:data ="" data)
+      ao_body.creator_uri   = user_uri
+      ao_body.content_type  = content_type
+      ao_body.name          = ao_body_path.split("/").last
+      ao_body.is_resource   = true
+      ao_body.is_aggregated = true
+
+      ao_body.save
+      # FIXME - no proxy is created for this ao:body resource
+
+      changed_descriptions << ao_body.path
+
+      annotation_rdf = create_rdf_xml do |graph|
+        graph << [annotation_uri, RDF.type, RO.AggregatedAnnotation]
+        graph << [annotation_uri, RDF.type, AO.Annotation]
+        graph << [annotation_uri, AO.body,  RDF::URI(ao_body.uri)]
+
+        request_links["http://purl.org/ao/annotatesResource"].each do |annotated_resource_uri|
+          graph << [annotation_uri, AO.annotatesResource, RDF::URI(annotated_resource_uri)]
+        end
+      end
+
+      annotation_stub = resources.new({
+        :creator_uri   => user_uri,
+        :path          => calculate_path(nil, 'application/vnd.wf4ever.annotation'),
+        :data          ="" annotation_rdf, # FIXME
+        :content_blob  => ContentBlob.new(:data ="" annotation_rdf),
+        :content_type  => 'application/vnd.wf4ever.annotation',
+        :is_annotation => true,
+        :ao_body_path  => ao_body.path
+      })
+
+      request_links["http://purl.org/ao/annotatesResource"].each do |annotated_resource_uri|
+        annotation_stub.annotation_resources.build(:resource_uri => relative_uri(annotated_resource_uri, uri))
+        links << { :link => annotated_resource_uri, :rel => "http://purl.org/ao/annotatesResource" }
+      end
+
+      annotation_stub.save
+
+      changed_descriptions << annotation_stub.path
+
+      links << { :link => annotation_stub.uri, :rel => "http://purl.org/ao/body" }
+
+      location = uri + annotation_stub.path
+
+    else
+
+      path = calculate_path(slug, content_type, request_links)
+
+      resource = resources.find_by_path(path)
+      resource = resources.new(:path => path) unless resource
+
+      resource.content_blob.destroy if resource.content_blob
+
+      resource.data          = "" # FIXME - to be removed
+      resource.content_blob  = ContentBlob.new(:data ="" data)
+      resource.creator_uri   = user_uri
+      resource.content_type  = content_type
+      resource.name          = path.split("/").last
+      resource.is_resource   = true
+      resource.is_aggregated = true
+
+      resource.save
+
+      changed_descriptions << resource.path
+    end
+
+    if resource && content_type != "application/vnd.wf4ever.proxy" && !resource.is_manifest? && !request_links["http://purl.org/ao/annotatesResource"]
+
+      resource_uri = resource.resource_uri.to_s
+
+      relative_resource_uri = relative_uri(resource_uri, uri)
+
+      proxy = resources.find(:first,
+          :conditions => { :content_type   => 'application/vnd.wf4ever.proxy',
+                           :proxy_in_path  => '.',
+                           :proxy_for_path => relative_resource_uri })
+
+      if proxy.nil?
+        proxy_slug = ".ro/proxies/#{SecureRandom.uuid}"
+      else
+        proxy_slug = proxy.path
+      end
+
+      proxy_body = pretty_rdf_xml(RDF::Writer.for(:rdfxml).buffer { |writer| writer << resource.generate_proxy(proxy_slug) } )
+
+      # FIXME - this should be a recursive call
+
+      proxy_attributes = {
+        :data           ="" proxy_body,
+        :content_blob   => ContentBlob.new(:data ="" proxy_body),
+        :proxy_in_path  => '.',
+        :proxy_for_path => relative_resource_uri,
+        :creator_uri    => user_uri,
+        :content_type   => 'application/vnd.wf4ever.proxy',
+        :is_proxy       => true,
+        :path           => proxy_slug
+      }
+
+      if proxy.nil?
+        proxy = resources.create(proxy_attributes)
+      else
+        proxy.content_blob.destroy
+        proxy.update_attributes(proxy_attributes)
+      end
+
+      links << { :link => resource_uri, :rel => "http://www.openarchives.org/ore/terms/proxyFor" }
+      location = proxy.uri
+
+      changed_descriptions << proxy_slug
+    end
+
+    location ||= resource_uri
+
+    [:created, nil, location, links, path, changed_descriptions]
+  end
+
+private
+
+  def create_manifest
+
+    resources.create(:path => ResearchObject::MANIFEST_PATH,
+                     :data ="" "Dummy content",
+                     :content_type => 'application/rdf+xml')
+
+    update_manifest!
+  end
+end

Added: branches/packs/app/models/resource.rb (0 => 3559)


--- branches/packs/app/models/resource.rb	                        (rev 0)
+++ branches/packs/app/models/resource.rb	2013-05-22 13:45:00 UTC (rev 3559)
@@ -0,0 +1,150 @@
+require 'securerandom'
+
+class Resource < ActiveRecord::Base
+
+  include ResearchObjectsHelper
+
+  belongs_to :research_object
+
+  belongs_to :content_blob, :dependent => :destroy
+
+  belongs_to :proxy_for,          :primary_key => :path, :foreign_key => :proxy_for_path,     :class_name => 'Resource'
+  has_one    :proxy,              :primary_key => :path, :foreign_key => :proxy_for_path,     :class_name => 'Resource'
+
+  belongs_to :proxy_in,           :primary_key => :path, :foreign_key => :proxy_in_path,      :class_name => 'Resource'
+  has_many   :proxies,            :primary_key => :path, :foreign_key => :proxy_in_path,      :class_name => 'Resource'
+
+  belongs_to :aggregated_by,      :primary_key => :path, :foreign_key => :aggregated_by_path, :class_name => 'Resource'
+  has_many   :aggregates,         :primary_key => :path, :foreign_key => :aggregated_by_path, :class_name => 'Resource'
+
+  belongs_to :ao_body,            :primary_key => :path, :foreign_key => :ao_body_path,       :class_name => 'Resource'
+  has_one    :ao_stub,            :primary_key => :path, :foreign_key => :ao_body_path,       :class_name => 'Resource'
+
+  belongs_to :resource_map,       :primary_key => :path, :foreign_key => :resource_map_path,  :class_name => 'Resource'
+  has_one    :is_resource_map_to, :primary_key => :path, :foreign_key => :resource_map_path,  :class_name => 'Resource'
+
+  has_many :annotation_resources, :foreign_key => 'annotation_id', :dependent => :destroy
+
+  validates_uniqueness_of :path, :scope => :research_object_id
+  validates_presence_of :content_type
+  validates_presence_of :path
+  validates_presence_of :content_blob
+
+  def is_manifest?
+    path == ResearchObject::MANIFEST_PATH
+  end
+
+  def description
+
+    graph = RDF::Graph.new
+
+    ro_uri = RDF::URI(research_object.uri)
+    uri    = resource_uri
+
+    if is_manifest?
+      graph << [uri, RDF.type, RDF::URI("http://purl.org/wf4ever/ro#Manifest")]
+      graph << [uri, RDF.type, RDF::URI("http://www.openarchives.org/ore/terms/ResourceMap")]
+      graph << [uri, RDF::URI("http://www.openarchives.org/ore/terms/describes"), ro_uri]
+    end
+
+    if is_resource
+      graph << [uri, RDF.type, RDF::URI("http://purl.org/wf4ever/ro#Resource")]
+    end
+
+    if is_aggregated
+      graph << [uri, RDF.type, RDF::URI("http://www.openarchives.org/ore/terms/AggregatedResource")]
+    end
+
+    if is_proxy
+      graph << [uri, RDF.type, RDF::URI("http://www.openarchives.org/ore/terms/Proxy")]
+      graph << [uri, RDF::URI("http://www.openarchives.org/ore/terms/proxyIn"), ro_uri.join(proxy_in_path)]
+      graph << [uri, RDF::URI("http://www.openarchives.org/ore/terms/proxyFor"), ro_uri.join(proxy_for_path)]
+    end
+
+    if is_annotation
+      graph << [uri, RDF.type, RDF::URI("http://purl.org/wf4ever/ro#AggregatedAnnotation")]
+      graph << [uri, RDF.type, RDF::URI("http://purl.org/ao/Annotation")]
+      graph << [uri, RDF::URI("http://purl.org/ao/body"), ro_uri.join(ao_body_path)]
+
+      annotation_resources.each do |resource|
+        graph << [uri, RDF::URI("http://purl.org/wf4ever/ro#annotatesAggregatedResource"), RDF::URI(resource.resource_uri)]
+      end
+    end
+
+    if is_resource_map
+
+      folder = is_resource_map_to
+
+      folder_uri = RDF::URI(folder.uri)
+
+      graph << [uri, RDF.type, ORE.ResourceMap]
+      graph << [uri, ORE.describes, folder_uri]
+
+      graph << [folder_uri, RDF.type, RO.folder]
+      graph << [folder_uri, RDF.type, ORE.Aggregation]
+      graph << [folder_uri, ORE.isDescribedBy, uri]
+      graph << [folder_uri, RDF::DC.created, folder.created_at.to_datetime]
+      graph << [folder_uri, RDF::DC.creator, RDF::URI(folder.creator_uri)]
+      graph << [folder_uri, ORE.isAggregatedBy, RDF::URI(folder.aggregated_by.uri)] if folder.aggregated_by_path
+
+      folder.aggregates.each do |aggregate|
+        graph << [folder_uri, ORE.aggregates, RDF::URI(aggregate.uri)]
+        graph << [RDF::URI(aggregate.uri), ORE.isAggregatedBy, folder_uri]
+      end
+    end
+
+    if is_folder
+      graph << [uri, RDF.type, RDF::URI("http://purl.org/wf4ever/ro#Resource")]
+      graph << [uri, RDF.type, RDF::URI("http://purl.org/wf4ever/ro#Folder")]
+      graph << [uri, RDF.type, RDF::URI("http://www.openarchives.org/ore/terms/AggregatedResource")]
+      graph << [uri, RDF.type, RDF::URI("http://www.openarchives.org/ore/terms/Aggregation")]
+
+      resource_map = Resource.find(:first, :conditions => { :path => resource_map_path })
+
+      graph << [uri, RDF::URI("http://www.openarchives.org/ore/terms/isDescribedBy"), ro_uri.join(resource_map_path)]
+    end
+
+    graph << [uri, RDF::DC.created, created_at.to_datetime]
+    graph << [uri, RDF::DC.creator, RDF::URI(creator_uri)] if creator_uri
+    graph << [uri, RDF::URI("http://purl.org/wf4ever/ro#name"), name] if name
+
+    if content_blob && !is_manifest?
+      graph << [uri, RDF::URI("http://purl.org/wf4ever/ro#filesize"), content_blob.size]
+      graph << [uri, RDF::URI("http://purl.org/wf4ever/ro#checksum"), RDF::URI("urn:MD5:#{content_blob.md5}")]
+    end
+
+    graph
+  end
+
+  def resource_uri
+    RDF::URI(research_object.uri) + path
+  end
+
+  def uri
+    RDF::URI(research_object.uri) + path
+  end
+
+  def generate_proxy(proxy_path = ".ro/proxies/#{SecureRandom.uuid}")
+
+    ro_uri    = RDF::URI(research_object.uri)
+    proxy_uri = RDF::URI(research_object.uri) + proxy_path
+
+    graph = RDF::Graph.new
+    graph << [proxy_uri, RDF.type,     ORE.Proxy]
+    graph << [proxy_uri, ORE.proxyIn,  ro_uri]
+    graph << [proxy_uri, ORE.proxyFor, resource_uri]
+
+    graph
+  end
+
+  def update_graph!
+
+    new_description = create_rdf_xml { |graph| graph << description }
+
+    content_blob.destroy if content_blob
+    content_blob = ContentBlob.new(:data ="" new_description)
+    content_blob.save
+
+    update_attribute(:data, new_description) # FIXME - to be removed
+  end
+end

Added: branches/packs/config/initializers/namespaces.rb (0 => 3559)


--- branches/packs/config/initializers/namespaces.rb	                        (rev 0)
+++ branches/packs/config/initializers/namespaces.rb	2013-05-22 13:45:00 UTC (rev 3559)
@@ -0,0 +1,39 @@
+require 'rdf'
+
+class AO < RDF::Vocabulary("http://purl.org/ao/")
+  # Declaring these might not be necessary
+  property :Annotation
+  property :body
+  property :annotatesResource
+end
+
+class ORE < RDF::Vocabulary("http://www.openarchives.org/ore/terms/")
+  property :Aggregation
+  property :AggregatedResource
+  property :Proxy
+  property :aggregates
+  property :proxyFor
+  property :proxyIn
+  property :isDescribedBy
+end
+
+class RO < RDF::Vocabulary("http://purl.org/wf4ever/ro#")
+  property :ResearchObject
+  property :AggregatedAnnotation
+  property :annotatesAggregatedResource
+  property :FolderEntry
+  property :Folder
+  property :Resource
+  property :entryName
+end
+
+class ROEVO < RDF::Vocabulary("http://purl.org/wf4ever/roevo#")
+  property :LiveRO
+end
+
+class ROTERMS < RDF::Vocabulary("http://ro.example.org/ro/terms/")
+  property :note
+  property :resource
+  property :defaultBase
+end
+

Modified: branches/packs/config/routes.rb (3558 => 3559)


--- branches/packs/config/routes.rb	2013-05-22 13:42:17 UTC (rev 3558)
+++ branches/packs/config/routes.rb	2013-05-22 13:45:00 UTC (rev 3559)
@@ -322,6 +322,41 @@
       :controller => 'linked_data', :action ="" 'taggings', :conditions => { :method => :get }
   end
 
+  # RODL routes.  There are no HTML pages for 'new' and 'edit' so the routes
+  # are generated without the resource helpers.
+
+  map.research_objects "/rodl/ROs", :controller => "research_objects", :action ="" "index",  :conditions => { :method => :get }
+  map.connect          "/rodl/ROs", :controller => "research_objects", :action ="" "create", :conditions => { :method => :post }
+
+  map.research_object "/rodl/ROs/:id", :controller => "research_objects", :action ="" "show",           :conditions => { :method => :get }
+  map.connect         "/rodl/ROs/:research_object_id", :controller => "resources",        :action ="" "post", :conditions => { :method => :post }
+  map.connect         "/rodl/ROs/:id", :controller => "research_objects", :action ="" "update",         :conditions => { :method => :put }
+  map.connect         "/rodl/ROs/:id", :controller => "research_objects", :action ="" "destroy",        :conditions => { :method => :delete }
+
+  map.named_route "research_object_resource", "/rodl/ROs/:research_object_id/:id",
+    :controller   => "resources",
+    :action       ="" "show",
+    :conditions   => { :method => :get },
+    :requirements => { :id => /.*/ }
+
+  map.connect "/rodl/ROs/:research_object_id/:id",
+    :controller   => "resources",
+    :action       ="" "update",
+    :conditions   => { :method => :put },
+    :requirements => { :id => /.*/ }
+
+  map.connect "/rodl/ROs/:research_object_id/:id",
+    :controller   => "resources",
+    :action       ="" "delete",
+    :conditions   => { :method => :delete },
+    :requirements => { :id => /.*/ }
+
+  map.connect "/rodl/ROs/:research_object_id/:path",
+    :controller   => "resources",
+    :action       ="" "post",
+    :conditions   => { :method => :post },
+    :requirements => { :path => /.*/ }
+
   # Install the default route as the lowest priority.
   map.connect ':controller/:action/:id'
 end

Added: branches/packs/db/migrate/20130520145900_create_research_objects.rb (0 => 3559)


--- branches/packs/db/migrate/20130520145900_create_research_objects.rb	                        (rev 0)
+++ branches/packs/db/migrate/20130520145900_create_research_objects.rb	2013-05-22 13:45:00 UTC (rev 3559)
@@ -0,0 +1,59 @@
+# myExperiment: db/migrate/20130520145900_create_research_objects.rb
+#
+# Copyright (c) 2007-2013 The University of Manchester, the University of
+# Oxford, and the University of Southampton.  See license.txt for details.
+
+class CreateResearchObjects < ActiveRecord::Migration
+  def self.up
+
+    create_table "research_objects", :force => true do |t|
+      t.string   "slug"
+      t.integer  "version"
+      t.string   "version_type"
+      t.integer  "user_id"
+
+      t.timestamps
+    end
+
+    create_table "resources" do |t|
+      t.string  "name"
+      t.integer "research_object_id"
+      t.integer "content_blob_id"
+      t.string  "sha1", :limit => 40
+      t.binary  "data", :limit => 2147483647
+      t.integer "size"
+      t.string  "content_type"
+      t.text    "path"
+      t.string  "creator_uri"
+      t.string  "proxy_in_path"
+      t.string  "proxy_for_path"
+      t.string  "ao_body_path"
+      t.string  "resource_map_path"
+      t.string  "aggregated_by_path"
+
+      t.boolean "is_resource",     :default => false
+      t.boolean "is_aggregated",   :default => false
+      t.boolean "is_proxy",        :default => false
+      t.boolean "is_annotation",   :default => false
+      t.boolean "is_resource_map", :default => false
+      t.boolean "is_folder",       :default => false
+      t.boolean "is_folder_entry", :default => false
+      t.boolean "is_root_folder",  :default => false
+
+      t.timestamps
+    end
+
+    create_table "annotation_resources" do |t|
+      t.integer "annotation_id"
+      t.string  "resource_uri"
+    end
+
+  end
+
+  def self.down
+    drop_table :research_objects
+    drop_table :resources
+    drop_table :annotation_resources
+  end
+
+end

Modified: branches/packs/db/schema.rb (3558 => 3559)


--- branches/packs/db/schema.rb	2013-05-22 13:42:17 UTC (rev 3558)
+++ branches/packs/db/schema.rb	2013-05-22 13:45:00 UTC (rev 3559)
@@ -9,7 +9,7 @@
 #
 # It's strongly recommended to check this file into your version control system.
 
-ActiveRecord::Schema.define(:version => 20130423091433) do
+ActiveRecord::Schema.define(:version => 20130520145900) do
 
   create_table "activities", :force => true do |t|
     t.string   "subject_type"
@@ -42,6 +42,11 @@
     t.datetime "promote_after"
   end
 
+  create_table "annotation_resources", :force => true do |t|
+    t.integer "annotation_id"
+    t.string  "resource_uri"
+  end
+
   create_table "announcements", :force => true do |t|
     t.string   "title"
     t.integer  "user_id"
@@ -155,9 +160,10 @@
   end
 
   create_table "content_blobs", :force => true do |t|
-    t.binary "data", :limit => 2147483647
-    t.string "md5",  :limit => 32
-    t.string "sha1", :limit => 40
+    t.binary  "data", :limit => 2147483647
+    t.string  "md5",  :limit => 32
+    t.string  "sha1", :limit => 40
+    t.integer "size"
   end
 
   add_index "content_blobs", ["md5"], :name => "index_content_blobs_on_md5"
@@ -595,6 +601,42 @@
     t.string  "workflow_uri"
   end
 
+  create_table "research_objects", :force => true do |t|
+    t.string   "slug"
+    t.integer  "version"
+    t.string   "version_type"
+    t.integer  "user_id"
+    t.datetime "created_at"
+    t.datetime "updated_at"
+  end
+
+  create_table "resources", :force => true do |t|
+    t.string   "name"
+    t.integer  "research_object_id"
+    t.integer  "content_blob_id"
+    t.string   "sha1",               :limit => 40
+    t.binary   "data",               :limit => 2147483647
+    t.integer  "size"
+    t.string   "content_type"
+    t.text     "path"
+    t.string   "creator_uri"
+    t.string   "proxy_in_path"
+    t.string   "proxy_for_path"
+    t.string   "ao_body_path"
+    t.string   "resource_map_path"
+    t.string   "aggregated_by_path"
+    t.boolean  "is_resource",                              :default => false
+    t.boolean  "is_aggregated",                            :default => false
+    t.boolean  "is_proxy",                                 :default => false
+    t.boolean  "is_annotation",                            :default => false
+    t.boolean  "is_resource_map",                          :default => false
+    t.boolean  "is_folder",                                :default => false
+    t.boolean  "is_folder_entry",                          :default => false
+    t.boolean  "is_root_folder",                           :default => false
+    t.datetime "created_at"
+    t.datetime "updated_at"
+  end
+
   create_table "reviews", :force => true do |t|
     t.string   "title",                         :default => ""
     t.text     "review"

Added: branches/packs/test/functional/research_objects_test.rb (0 => 3559)


--- branches/packs/test/functional/research_objects_test.rb	                        (rev 0)
+++ branches/packs/test/functional/research_objects_test.rb	2013-05-22 13:45:00 UTC (rev 3559)
@@ -0,0 +1,419 @@
+require_relative '../test_helper'
+
+class ResearchObjectsTest < ActionController::IntegrationTest
+
+  include ResearchObjectsHelper
+
+  fixtures :all
+
+  def setup
+    host!("test.host")
+  end
+
+  test "creation and deletion of research objects" do
+
+    ros_uri      = research_objects_url
+    ro_uri       = research_object_url('test_ro') + "/"
+    manifest_uri = ro_uri + ResearchObject::MANIFEST_PATH
+
+    # Test that the index of research objects is empty.
+
+    get research_objects_path
+
+    assert_response :ok
+    assert_equal "", @response.body
+    assert_equal "text/uri-list", @response.content_type
+
+    # Create a research object.
+
+    post research_objects_path, {}, { "HTTP_SLUG" => "test_ro" }
+
+    assert_response :created
+    assert_equal ro_uri, @response.headers["Location"]
+
+    # Test that you can't create another RO at the same location
+
+    post research_objects_path, {}, { "HTTP_SLUG" => "test_ro" }
+
+    assert_response :conflict
+
+    # Test that the index now contains the new research object.
+
+    get research_objects_path
+
+    assert_response :ok
+    assert_equal "#{ro_uri}\n", @response.body
+    assert_equal "text/uri-list", @response.content_type
+
+    # Test the manifest redirection.
+
+    get ro_uri, {}, { "HTTP_ACCEPT" => "application/rdf+xml" }
+
+    assert_response :see_other
+    assert_equal manifest_uri, @response.headers["Location"]
+
+    # Get the manifest.
+
+    get manifest_uri
+
+    assert_response :ok
+
+    # Delete the test RO.
+
+    delete ro_uri
+    
+    assert_response :no_content
+ 
+    # Try to get the deleted research object.
+
+    get ro_uri
+
+    assert_response :not_found
+
+    # Try to get the manifest of the deleted research object.
+
+    get manifest_uri
+
+    assert_response :not_found
+
+    # Delete the non-existent RO.
+    
+    delete ro_uri
+    
+    assert_response :not_found
+
+  end
+
+  test "manifest statements of a newly created research object" do
+
+    # Create a new research object and get the manifest
+
+    post research_objects_path, {}, { "HTTP_SLUG" => "test_ro" }
+
+    assert_response :created
+    assert_equal research_object_url('test_ro') + "/", @response.headers["Location"]
+
+    get research_object_resource_path('test_ro', '.ro/manifest.rdf')
+
+    assert_response :ok
+
+    # Parse the graph and ensure that it has statements
+
+    graph = RDF::Graph.new
+    graph << RDF::Reader.for(:rdfxml).new(@response.body)
+
+    ro_uri = RDF::URI(research_object_url('test_ro')) + "/"
+
+    assert_operator 0, :<, graph.count
+
+    # Test that the creator of the RO is correct
+
+    assert_equal user_url(users(:john)), graph.query([ro_uri, RDF::DC.creator, nil]).first_object
+
+    # Test that the date has the correct datatype and within reason
+
+    assert_equal RDF::XSD.dateTime, graph.query([ro_uri, RDF::DC.created, nil]).first_object.datatype
+
+    created = DateTime.parse(graph.query([ro_uri, RDF::DC.created, nil]).first_object.value)
+
+    assert_operator DateTime.parse("2000-01-01"), :<, created
+    assert_operator DateTime.parse("3000-01-01"), :>, created
+
+  end
+
+  test "automatic generation of proxies for generic resources" do
+
+    # Create the test RO.
+
+    post research_objects_path, {}, { "HTTP_SLUG" => "test_ro" }
+    assert_response :created
+
+    # Create the resource and check the response is the proxy
+
+    post research_object_path('test_ro'), "Hello World.\n", { "HTTP_SLUG" => "test_resource.txt", "CONTENT_TYPE" => "text/plain" }
+
+    assert_response :created
+    assert_equal "application/rdf+xml", @response.content_type.to_s
+
+    links = parse_links(@response.headers)
+
+    resource_uri = links["http://www.openarchives.org/ore/terms/proxyFor"].first
+
+    assert_equal research_object_resource_url("test_ro", "test_resource.txt"), resource_uri
+
+    graph = RDF::Graph.new
+    graph << RDF::Reader.for(:rdfxml).new(@response.body)
+
+    assert_equal 1, graph.query([nil, RDF.type, RDF::URI("http://www.openarchives.org/ore/terms/Proxy")]).count
+
+    proxy_uri = graph.query([nil, RDF::URI("http://www.openarchives.org/ore/terms/proxyFor"), RDF::URI(resource_uri)]).first_subject.to_s
+
+    # Confirm that the resource was created.
+
+    get resource_uri
+
+    assert_response :ok
+    assert_equal "Hello World.\n", @response.body
+    assert_equal "text/plain", @response.content_type.to_s
+
+    # Confirm that a proxy was created automatically and that the proxy graph is correct.
+
+    get proxy_uri
+
+    assert_response :ok
+    assert_equal "application/vnd.wf4ever.proxy", @response.content_type.to_s
+
+    graph2 = RDF::Graph.new
+    graph2 << RDF::Reader.for(:rdfxml).new(@response.body)
+
+    assert graph2.query([
+        RDF::URI(proxy_uri),
+        RDF::URI("http://www.openarchives.org/ore/terms/proxyFor"),
+        RDF::URI(resource_uri)
+        ])
+
+    assert graph2.query([
+        RDF::URI(proxy_uri),
+        RDF.type,
+        RDF::URI("http://www.openarchives.org/ore/terms/Proxy")
+        ])
+
+    assert graph2.query([
+        RDF::URI(proxy_uri),
+        RDF::URI("http://www.openarchives.org/ore/terms/proxyIn"),
+        RDF::URI(research_object_url("test_ro") + "/")
+        ])
+  end
+
+  test "Disallow overwriting of the manifest" do
+
+    # Create the test RO.
+
+    post research_objects_path, {}, { "HTTP_SLUG" => "test_ro" }
+    assert_response :created
+
+    # Create a resource and try to overwrite the manifest
+
+    post research_object_path('test_ro'), "Hello World.\n", { "HTTP_SLUG" => ResearchObject::MANIFEST_PATH, "CONTENT_TYPE" => "text/plain" }
+    assert_response :forbidden
+  end
+
+  test "Creation of a proxy for an external resource" do
+
+    # Create the test RO.
+
+    post research_objects_path, {}, { "HTTP_SLUG" => "test_ro" }
+
+    assert_response :created
+
+    ro_uri = @response.location
+
+    # Create a proxy by providing a proxy description with an ore:proxyFor term.
+
+    external_resource_uri = "http://elsewhere.example.com/external_resource.txt"
+
+    test_proxy_description = %Q(
+      <rdf:RDF
+          xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+          xmlns:ore="http://www.openarchives.org/ore/terms/" >
+
+        <rdf:Description>
+          <ore:proxyFor rdf:resource="#{external_resource_uri}"/>
+          <rdf:type rdf:resource="http://www.openarchives.org/ore/terms/Proxy"/>
+        </rdf:Description>
+
+      </rdf:RDF>
+    )
+
+    post(research_object_path('test_ro'),
+        test_proxy_description,
+        { "CONTENT_TYPE" => "application/vnd.wf4ever.proxy" })
+
+    links = parse_links(@response.headers)
+
+    proxy_for_uri = links["http://www.openarchives.org/ore/terms/proxyFor"].first
+    proxy_uri = @response.location
+
+    graph = load_graph(@response.body)
+
+    # Check ore:proxyFor link
+
+    assert_equal(external_resource_uri, proxy_for_uri)
+
+    # Check the returned RDF for the appropriate proxy statements
+
+    assert_equal 1, graph.query([RDF::URI(proxy_uri), RDF.type, ORE.Proxy]).count
+    assert_equal 1, graph.query([RDF::URI(proxy_uri), ORE.proxyFor, RDF::URI(proxy_for_uri)]).count
+    assert_equal 1, graph.query([RDF::URI(proxy_uri), ORE.proxyIn, RDF::URI(ro_uri)]).count
+
+    # Check the manifest for the appropriate proxy statements
+
+    get ro_uri + ResearchObject::MANIFEST_PATH
+    assert_response :ok
+
+    manifest_graph = load_graph(@response.body)
+
+    assert_equal 1, manifest_graph.query([RDF::URI(proxy_uri), RDF.type, ORE.Proxy]).count
+    assert_equal 1, manifest_graph.query([RDF::URI(proxy_uri), ORE.proxyFor, RDF::URI(proxy_for_uri)]).count
+    assert_equal 1, manifest_graph.query([RDF::URI(proxy_uri), ORE.proxyIn, RDF::URI(ro_uri)]).count
+  end
+
+  test "Creation of a folder" do
+
+    # Create the test RO.
+
+    post research_objects_path, {}, { "HTTP_SLUG" => "test_ro" }
+
+    assert_response :created
+
+    ro_uri = @response.location
+
+    # Create the folder
+
+    test_folder_description = %Q(
+      <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" >
+        <rdf:Description>
+          <rdf:type rdf:resource="http://purl.org/wf4ever/ro#Folder"/>
+        </rdf:Description>
+      </rdf:RDF>
+    )
+
+    test_folder_path = "test_folder/"
+
+    test_folder_uri = RDF::URI(ro_uri) + test_folder_path
+
+    post(research_object_path('test_ro'),
+        test_folder_description,
+        { "CONTENT_TYPE" => "application/vnd.wf4ever.folder",
+          "SLUG"         =>  test_folder_path })
+
+    assert_response :created
+
+    links = parse_links(@response.headers)
+
+    proxy_for_uri   = links["http://www.openarchives.org/ore/terms/proxyFor"].first
+    is_described_by = links["http://www.openarchives.org/ore/terms/isDescribedBy"].first
+
+    proxy_uri = @response.location
+
+    graph = load_graph(@response.body)
+
+    resource_map_uri = graph.query([test_folder_uri, ORE.isDescribedBy, nil]).first_object
+
+    assert resource_map_uri
+
+    # Assert the link and location headers match up with the response RDF
+
+    assert_equal 1, graph.query([RDF::URI(proxy_uri), RDF.type, ORE.Proxy]).count
+    assert_equal 1, graph.query([RDF::URI(proxy_uri), ORE.proxyFor, test_folder_uri]).count
+    assert_equal 1, graph.query([resource_map_uri, ORE.describes, test_folder_uri]).count
+
+    # Assert the statements in the manifest.
+
+    get ro_uri + ResearchObject::MANIFEST_PATH
+    assert_response :ok
+
+    manifest_graph = load_graph(@response.body)
+
+    assert_equal 1, manifest_graph.query([RDF::URI(proxy_uri), RDF.type, ORE.Proxy]).count
+    assert_equal 1, manifest_graph.query([RDF::URI(proxy_uri), ORE.proxyFor, test_folder_uri]).count
+    assert_equal 1, manifest_graph.query([resource_map_uri, ORE.describes, test_folder_uri]).count
+  end
+
+  test "Creation of a folder entry" do
+
+    # Create the test RO.
+
+    post research_objects_path, {}, { "HTTP_SLUG" => "test_ro" }
+
+    assert_response :created
+
+    ro_uri = @response.location
+
+    # Create the folder
+
+    test_folder_description = %Q(
+      <rdf:RDF xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" >
+        <rdf:Description>
+          <rdf:type rdf:resource="http://purl.org/wf4ever/ro#Folder"/>
+        </rdf:Description>
+      </rdf:RDF>
+    )
+
+    test_folder_path = "test_folder/"
+
+    test_folder_uri = RDF::URI(ro_uri) + test_folder_path
+
+    post(research_object_path('test_ro'),
+        test_folder_description,
+        { "CONTENT_TYPE" => "application/vnd.wf4ever.folder",
+          "SLUG"         =>  test_folder_path })
+
+    assert_response :created
+
+    # Create the folder entry
+
+    test_folder_description = %Q(
+      <rdf:RDF
+          xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+          xmlns:ore="http://www.openarchives.org/ore/terms/" >
+
+        <rdf:Description>
+          <rdf:type rdf:resource="http://purl.org/wf4ever/ro#FolderEntry"/>
+          <ore:proxyFor rdf:resource="#{test_folder_uri}"/>
+        </rdf:Description>
+      </rdf:RDF>
+    )
+
+
+#   proxy_for_uri   = links["http://www.openarchives.org/ore/terms/proxyFor"].first
+#   is_described_by = links["http://www.openarchives.org/ore/terms/isDescribedBy"].first
+
+#   proxy_uri = @response.location
+
+#   graph = load_graph(@response.body)
+
+#   resource_map_uri = graph.query([test_folder_uri, ORE.isDescribedBy, nil]).first_object
+
+#   assert resource_map_uri
+
+#   # Assert the link and location headers match up with the response RDF
+
+#   assert_equal 1, graph.query([RDF::URI(proxy_uri), RDF.type, ORE.Proxy]).count
+#   assert_equal 1, graph.query([RDF::URI(proxy_uri), ORE.proxyFor, test_folder_uri]).count
+#   assert_equal 1, graph.query([resource_map_uri, ORE.describes, test_folder_uri]).count
+
+#   # Assert the statements in the manifest.
+
+#   get ro_uri + ResearchObject::MANIFEST_PATH
+#   assert_response :ok
+
+#   manifest_graph = load_graph(@response.body)
+
+#   assert_equal 1, manifest_graph.query([RDF::URI(proxy_uri), RDF.type, ORE.Proxy]).count
+#   assert_equal 1, manifest_graph.query([RDF::URI(proxy_uri), ORE.proxyFor, test_folder_uri]).count
+#   assert_equal 1, manifest_graph.query([resource_map_uri, ORE.describes, test_folder_uri]).count
+  end
+
+  def parse_links(headers)
+
+    links = {}
+
+    link_headers = headers["Link"]
+
+    return {} if link_headers.nil?
+
+    if link_headers
+      link_headers.each do |link|
+        matches = link.strip.match(/<([^>]*)>\s*;.*rel\s*=\s*"?([^;"]*)"?/)
+        if matches
+          links[matches[2]] ||= []
+          links[matches[2]] << matches[1]
+        end
+      end
+    end
+
+    links
+  end
+
+end

reply via email to

[Prev in Thread] Current Thread [Next in Thread]