The bug allows a newly-created element to refer to a deleted one if the transactions for both overlap. Precisely, the issue is that the check that an element exists does not prevent a concurrent transaction from altering that row. Because "deleting" an element in the OSM database does not remove the row, we cannot rely on FK constraints to ensure the correct behaviour. Instead, this fix relies on manually locking referenced elements. Note that this "fix" is suboptimal, as it does not allow any updates to the referenced elements. Updates which do not delete the row could safely be done, but will be prevented. Also, it's not clear what the negative performance impact of this change will be.
309 lines
9.2 KiB
Ruby
309 lines
9.2 KiB
Ruby
class Way < ActiveRecord::Base
|
|
require "xml/libxml"
|
|
|
|
include ConsistencyValidations
|
|
include NotRedactable
|
|
include ObjectMetadata
|
|
|
|
self.table_name = "current_ways"
|
|
|
|
belongs_to :changeset
|
|
|
|
has_many :old_ways, -> { order(:version) }
|
|
|
|
has_many :way_nodes, -> { order(:sequence_id) }
|
|
has_many :nodes, :through => :way_nodes
|
|
|
|
has_many :way_tags
|
|
|
|
has_many :containing_relation_members, :class_name => "RelationMember", :as => :member
|
|
has_many :containing_relations, :class_name => "Relation", :through => :containing_relation_members, :source => :relation
|
|
|
|
validates :id, :uniqueness => true, :presence => { :on => :update },
|
|
:numericality => { :on => :update, :integer_only => true }
|
|
validates :version, :presence => true,
|
|
:numericality => { :integer_only => true }
|
|
validates :changeset_id, :presence => true,
|
|
:numericality => { :integer_only => true }
|
|
validates :timestamp, :presence => true
|
|
validates :changeset, :associated => true
|
|
validates :visible, :inclusion => [true, false]
|
|
|
|
scope :visible, -> { where(:visible => true) }
|
|
scope :invisible, -> { where(:visible => false) }
|
|
|
|
# Read in xml as text and return it's Way object representation
|
|
def self.from_xml(xml, create = false)
|
|
p = XML::Parser.string(xml)
|
|
doc = p.parse
|
|
|
|
doc.find("//osm/way").each do |pt|
|
|
return Way.from_xml_node(pt, create)
|
|
end
|
|
fail OSM::APIBadXMLError.new("node", xml, "XML doesn't contain an osm/way element.")
|
|
rescue LibXML::XML::Error, ArgumentError => ex
|
|
raise OSM::APIBadXMLError.new("way", xml, ex.message)
|
|
end
|
|
|
|
def self.from_xml_node(pt, create = false)
|
|
way = Way.new
|
|
|
|
fail OSM::APIBadXMLError.new("way", pt, "Version is required when updating") unless create || !pt["version"].nil?
|
|
way.version = pt["version"]
|
|
fail OSM::APIBadXMLError.new("way", pt, "Changeset id is missing") if pt["changeset"].nil?
|
|
way.changeset_id = pt["changeset"]
|
|
|
|
unless create
|
|
fail OSM::APIBadXMLError.new("way", pt, "ID is required when updating") if pt["id"].nil?
|
|
way.id = pt["id"].to_i
|
|
# .to_i will return 0 if there is no number that can be parsed.
|
|
# We want to make sure that there is no id with zero anyway
|
|
fail OSM::APIBadUserInput.new("ID of way cannot be zero when updating.") if way.id == 0
|
|
end
|
|
|
|
# We don't care about the timestamp nor the visibility as these are either
|
|
# set explicitly or implicit in the action. The visibility is set to true,
|
|
# and manually set to false before the actual delete.
|
|
way.visible = true
|
|
|
|
# Start with no tags
|
|
way.tags = {}
|
|
|
|
# Add in any tags from the XML
|
|
pt.find("tag").each do |tag|
|
|
fail OSM::APIBadXMLError.new("way", pt, "tag is missing key") if tag["k"].nil?
|
|
fail OSM::APIBadXMLError.new("way", pt, "tag is missing value") if tag["v"].nil?
|
|
way.add_tag_keyval(tag["k"], tag["v"])
|
|
end
|
|
|
|
pt.find("nd").each do |nd|
|
|
way.add_nd_num(nd["ref"])
|
|
end
|
|
|
|
way
|
|
end
|
|
|
|
# Find a way given it's ID, and in a single SQL call also grab its nodes and tags
|
|
def to_xml
|
|
doc = OSM::API.new.get_xml_doc
|
|
doc.root << to_xml_node
|
|
doc
|
|
end
|
|
|
|
def to_xml_node(visible_nodes = nil, changeset_cache = {}, user_display_name_cache = {})
|
|
el = XML::Node.new "way"
|
|
el["id"] = id.to_s
|
|
|
|
add_metadata_to_xml_node(el, self, changeset_cache, user_display_name_cache)
|
|
|
|
# make sure nodes are output in sequence_id order
|
|
ordered_nodes = []
|
|
way_nodes.each do |nd|
|
|
if visible_nodes
|
|
# if there is a list of visible nodes then use that to weed out deleted nodes
|
|
if visible_nodes[nd.node_id]
|
|
ordered_nodes[nd.sequence_id] = nd.node_id.to_s
|
|
end
|
|
else
|
|
# otherwise, manually go to the db to check things
|
|
if nd.node && nd.node.visible?
|
|
ordered_nodes[nd.sequence_id] = nd.node_id.to_s
|
|
end
|
|
end
|
|
end
|
|
|
|
ordered_nodes.each do |nd_id|
|
|
next unless nd_id && nd_id != "0"
|
|
|
|
node_el = XML::Node.new "nd"
|
|
node_el["ref"] = nd_id
|
|
el << node_el
|
|
end
|
|
|
|
add_tags_to_xml_node(el, way_tags)
|
|
|
|
el
|
|
end
|
|
|
|
def nds
|
|
@nds ||= way_nodes.collect(&:node_id)
|
|
end
|
|
|
|
def tags
|
|
@tags ||= Hash[way_tags.collect { |t| [t.k, t.v] }]
|
|
end
|
|
|
|
attr_writer :nds
|
|
|
|
attr_writer :tags
|
|
|
|
def add_nd_num(n)
|
|
@nds = [] unless @nds
|
|
@nds << n.to_i
|
|
end
|
|
|
|
def add_tag_keyval(k, v)
|
|
@tags = {} unless @tags
|
|
|
|
# duplicate tags are now forbidden, so we can't allow values
|
|
# in the hash to be overwritten.
|
|
fail OSM::APIDuplicateTagsError.new("way", id, k) if @tags.include? k
|
|
|
|
@tags[k] = v
|
|
end
|
|
|
|
##
|
|
# the integer coords (i.e: unscaled) bounding box of the way, assuming
|
|
# straight line segments.
|
|
def bbox
|
|
lons = nodes.collect(&:longitude)
|
|
lats = nodes.collect(&:latitude)
|
|
BoundingBox.new(lons.min, lats.min, lons.max, lats.max)
|
|
end
|
|
|
|
def update_from(new_way, user)
|
|
Way.transaction do
|
|
self.lock!
|
|
check_consistency(self, new_way, user)
|
|
unless new_way.preconditions_ok?(nds)
|
|
fail OSM::APIPreconditionFailedError.new("Cannot update way #{id}: data is invalid.")
|
|
end
|
|
|
|
self.changeset_id = new_way.changeset_id
|
|
self.changeset = new_way.changeset
|
|
self.tags = new_way.tags
|
|
self.nds = new_way.nds
|
|
self.visible = true
|
|
save_with_history!
|
|
end
|
|
end
|
|
|
|
def create_with_history(user)
|
|
check_create_consistency(self, user)
|
|
unless self.preconditions_ok?
|
|
fail OSM::APIPreconditionFailedError.new("Cannot create way: data is invalid.")
|
|
end
|
|
self.version = 0
|
|
self.visible = true
|
|
save_with_history!
|
|
end
|
|
|
|
def preconditions_ok?(old_nodes = [])
|
|
return false if nds.empty?
|
|
if nds.length > MAX_NUMBER_OF_WAY_NODES
|
|
fail OSM::APITooManyWayNodesError.new(id, nds.length, MAX_NUMBER_OF_WAY_NODES)
|
|
end
|
|
|
|
# check only the new nodes, for efficiency - old nodes having been checked last time and can't
|
|
# be deleted when they're in-use.
|
|
new_nds = (nds - old_nodes).sort.uniq
|
|
|
|
unless new_nds.empty?
|
|
# NOTE: nodes are locked here to ensure they can't be deleted before
|
|
# the current transaction commits.
|
|
db_nds = Node.where(:id => new_nds, :visible => true).lock("for share")
|
|
|
|
if db_nds.length < new_nds.length
|
|
missing = new_nds - db_nds.collect(&:id)
|
|
fail OSM::APIPreconditionFailedError.new("Way #{id} requires the nodes with id in (#{missing.join(',')}), which either do not exist, or are not visible.")
|
|
end
|
|
end
|
|
|
|
true
|
|
end
|
|
|
|
def delete_with_history!(new_way, user)
|
|
fail OSM::APIAlreadyDeletedError.new("way", new_way.id) unless visible
|
|
|
|
# need to start the transaction here, so that the database can
|
|
# provide repeatable reads for the used-by checks. this means it
|
|
# shouldn't be possible to get race conditions.
|
|
Way.transaction do
|
|
self.lock!
|
|
check_consistency(self, new_way, user)
|
|
rels = Relation.joins(:relation_members).where(:visible => true, :current_relation_members => { :member_type => "Way", :member_id => id }).order(:id)
|
|
fail OSM::APIPreconditionFailedError.new("Way #{id} is still used by relations #{rels.collect(&:id).join(",")}.") unless rels.empty?
|
|
|
|
self.changeset_id = new_way.changeset_id
|
|
self.changeset = new_way.changeset
|
|
|
|
self.tags = []
|
|
self.nds = []
|
|
self.visible = false
|
|
save_with_history!
|
|
end
|
|
end
|
|
|
|
##
|
|
# if any referenced nodes are placeholder IDs (i.e: are negative) then
|
|
# this calling this method will fix them using the map from placeholders
|
|
# to IDs +id_map+.
|
|
def fix_placeholders!(id_map, placeholder_id = nil)
|
|
nds.map! do |node_id|
|
|
if node_id < 0
|
|
new_id = id_map[:node][node_id]
|
|
fail OSM::APIBadUserInput.new("Placeholder node not found for reference #{node_id} in way #{id.nil? ? placeholder_id : id}") if new_id.nil?
|
|
new_id
|
|
else
|
|
node_id
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def save_with_history!
|
|
t = Time.now.getutc
|
|
|
|
# update the bounding box, note that this has to be done both before
|
|
# and after the save, so that nodes from both versions are included in the
|
|
# bbox. we use a copy of the changeset so that it isn't reloaded
|
|
# later in the save.
|
|
cs = changeset
|
|
cs.update_bbox!(bbox) unless nodes.empty?
|
|
|
|
Way.transaction do
|
|
self.version += 1
|
|
self.timestamp = t
|
|
self.save!
|
|
|
|
tags = self.tags
|
|
WayTag.delete_all(:way_id => id)
|
|
tags.each do |k, v|
|
|
tag = WayTag.new
|
|
tag.way_id = id
|
|
tag.k = k
|
|
tag.v = v
|
|
tag.save!
|
|
end
|
|
|
|
nds = self.nds
|
|
WayNode.delete_all(:way_id => id)
|
|
sequence = 1
|
|
nds.each do |n|
|
|
nd = WayNode.new
|
|
nd.id = [id, sequence]
|
|
nd.node_id = n
|
|
nd.save!
|
|
sequence += 1
|
|
end
|
|
|
|
old_way = OldWay.from_way(self)
|
|
old_way.timestamp = t
|
|
old_way.save_with_dependencies!
|
|
|
|
# reload the way so that the nodes array points to the correct
|
|
# new set of nodes.
|
|
reload
|
|
|
|
# update and commit the bounding box, now that way nodes
|
|
# have been updated and we're in a transaction.
|
|
cs.update_bbox!(bbox) unless nodes.empty?
|
|
|
|
# tell the changeset we updated one element only
|
|
cs.add_changes! 1
|
|
|
|
cs.save!
|
|
end
|
|
end
|
|
end
|