summaryrefslogtreecommitdiff
path: root/src/library/scala/xml/dtd/ElementValidator.scala
blob: 1d6fd0119adcd7477f4da9622cef03d1c911aceb (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
/*                     __                                               *\
**     ________ ___   / /  ___     Scala API                            **
**    / __/ __// _ | / /  / _ |    (c) 2002-2009, LAMP/EPFL             **
**  __\ \/ /__/ __ |/ /__/ __ |    http://www.scala-lang.org/           **
** /____/\___/_/ |_/____/_/ | |                                         **
**                          |/                                          **
\*                                                                      */

// $Id$


package scala.xml
package dtd

import PartialFunction._
import ContentModel.ElemName
import MakeValidationException._    // @todo other exceptions

import scala.util.automata._
import scala.collection.mutable.BitSet

/** validate children and/or attributes of an element
 *  exceptions are created but not thrown.
 */
class ElementValidator() extends Function1[Node,Boolean] {

  private var exc: List[ValidationException] = Nil

  protected var contentModel: ContentModel           = _
  protected var dfa:          DetWordAutom[ElemName] = _
  protected var adecls:       List[AttrDecl]         = _

  /** set content model, enabling element validation */
  def setContentModel(cm: ContentModel) = {
    contentModel = cm
    cm match {
      case ELEMENTS(r) =>
        val nfa = ContentModel.Translator.automatonFrom(r, 1)
        dfa = new SubsetConstruction(nfa).determinize
      case _ =>
        dfa = null
    }
  }

  def getContentModel = contentModel

  /** set meta data, enabling attribute validation */
  def setMetaData(adecls: List[AttrDecl]) { this.adecls = adecls }

  def getIterable(nodes: Seq[Node], skipPCDATA: Boolean): Iterable[ElemName] = {
    def isAllWhitespace(a: Atom[_]) = cond(a.data) { case s: String if s.trim.isEmpty  => true }

    nodes.filter {
      case y: SpecialNode => y match {
        case a: Atom[_] if isAllWhitespace(a) => false  // always skip all-whitespace nodes
        case _                                => !skipPCDATA
      }
      case x                                  => x.namespace eq null
    } . map (x => ElemName(x.label))
  }

  /** check attributes, return true if md corresponds to attribute declarations in adecls.
   */
  def check(md: MetaData): Boolean = {
    val len: Int = exc.length
    var ok = new BitSet(adecls.length)

    for (attr <- md) {
      def attrStr = attr.value.toString
      def find(Key: String): Option[AttrDecl] = {
        adecls.zipWithIndex find {
          case (a @ AttrDecl(Key, _, _), j) => ok += j ; return Some(a)
          case _                            => false
        }
        None
      }

      find(attr.key) match {
        case None =>
          exc ::= fromUndefinedAttribute(attr.key)

        case Some(AttrDecl(_, tpe, DEFAULT(true, fixedValue))) if attrStr != fixedValue =>
          exc ::= fromFixedAttribute(attr.key, fixedValue, attrStr)

        case _ =>
      }
    }

    adecls.zipWithIndex foreach {
      case (AttrDecl(key, tpe, REQUIRED), j) if !ok(j) => exc ::= fromMissingAttribute(key, tpe)
      case _ =>
    }

    exc.length == len //- true if no new exception
  }

  /** check children, return true if conform to content model
   *  @pre contentModel != null
   */
  def check(nodes: Seq[Node]): Boolean = contentModel match {
    case ANY    => true
    case EMPTY  => getIterable(nodes, false).isEmpty
    case PCDATA => getIterable(nodes, true).isEmpty
    case MIXED(ContentModel.Alt(branches @ _*))  =>   // @todo
      val j = exc.length
      def find(Key: String): Boolean =
        branches exists { case ContentModel.Letter(ElemName(Key)) => true ; case _ => false }

      getIterable(nodes, true) map (_.name) filterNot find foreach {
        exc ::= MakeValidationException fromUndefinedElement _
      }
      (exc.length == j)   // - true if no new exception

    case _: ELEMENTS =>
      dfa isFinal {
        getIterable(nodes, false).foldLeft(0) { (q, e) =>
          (dfa delta q).getOrElse(e, throw ValidationException("element %s not allowed here" format e))
        }
      }
  }

  /** applies various validations - accumulates error messages in exc
   *  @todo: fail on first error, ignore other errors (rearranging conditions)
   */
  def apply(n: Node): Boolean =
    //- ? check children
    ((contentModel == null) || check(n.child)) &&
    //- ? check attributes
    ((adecls == null) || check(n.attributes))
}