1 /* 2 * Copyright (c) 1997, 2015, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.sun.tools.internal.xjc.reader.internalizer; 27 28 import java.net.MalformedURLException; 29 import java.net.URL; 30 import java.util.ArrayList; 31 import java.util.HashMap; 32 import java.util.HashSet; 33 import java.util.List; 34 import java.util.Map; 35 import java.util.Set; 36 import java.text.ParseException; 37 38 import javax.xml.xpath.XPath; 39 import javax.xml.xpath.XPathConstants; 40 import javax.xml.xpath.XPathExpressionException; 41 import javax.xml.xpath.XPathFactory; 42 43 import com.sun.istack.internal.SAXParseException2; 44 import com.sun.istack.internal.NotNull; 45 import com.sun.istack.internal.Nullable; 46 import com.sun.tools.internal.xjc.ErrorReceiver; 47 import com.sun.tools.internal.xjc.reader.Const; 48 import com.sun.tools.internal.xjc.util.DOMUtils; 49 import com.sun.xml.internal.bind.v2.util.EditDistance; 50 import com.sun.xml.internal.bind.v2.util.XmlFactory; 51 import com.sun.xml.internal.xsom.SCD; 52 import java.io.File; 53 import java.io.IOException; 54 import java.util.logging.Level; 55 import java.util.logging.Logger; 56 import javax.xml.XMLConstants; 57 58 import org.w3c.dom.Attr; 59 import org.w3c.dom.Document; 60 import org.w3c.dom.Element; 61 import org.w3c.dom.NamedNodeMap; 62 import org.w3c.dom.Node; 63 import org.w3c.dom.NodeList; 64 import org.xml.sax.SAXParseException; 65 66 /** 67 * Internalizes external binding declarations. 68 * 69 * <p> 70 * The {@link #transform(DOMForest,boolean)} method is the entry point. 71 * 72 * @author 73 * Kohsuke Kawaguchi (kohsuke.kawaguchi@sun.com) 74 */ 75 class Internalizer { 76 77 private static final String WSDL_NS = "http://schemas.xmlsoap.org/wsdl/"; 78 79 private final XPath xpath; 80 81 /** 82 * Internalize all {@code <jaxb:bindings>} customizations in the given forest. 83 * 84 * @return 85 * if the SCD support is enabled, the return bindings need to be applied 86 * after schema components are parsed. 87 * If disabled, the returned binding set will be empty. 88 * SCDs are only for XML Schema, and doesn't make any sense for other 89 * schema languages. 90 */ 91 static SCDBasedBindingSet transform( DOMForest forest, boolean enableSCD, boolean disableSecureProcessing ) { 92 return new Internalizer(forest, enableSCD, disableSecureProcessing).transform(); 93 } 94 95 96 private Internalizer(DOMForest forest, boolean enableSCD, boolean disableSecureProcessing) { 97 this.errorHandler = forest.getErrorHandler(); 98 this.forest = forest; 99 this.enableSCD = enableSCD; 100 xpath = XmlFactory.createXPathFactory(disableSecureProcessing).newXPath(); 101 } 102 103 /** 104 * DOMForest object currently being processed. 105 */ 106 private final DOMForest forest; 107 108 /** 109 * All errors found during the transformation is sent to this object. 110 */ 111 private ErrorReceiver errorHandler; 112 113 /** 114 * If true, the SCD-based target selection is supported. 115 */ 116 private boolean enableSCD; 117 118 119 private SCDBasedBindingSet transform() { 120 121 // either target nodes are conventional DOM nodes (as per spec), 122 Map<Element,List<Node>> targetNodes = new HashMap<Element,List<Node>>(); 123 // ... or it will be schema components by means of SCD (RI extension) 124 SCDBasedBindingSet scd = new SCDBasedBindingSet(forest); 125 126 // 127 // identify target nodes for all <jaxb:bindings> 128 // 129 for (Element jaxbBindings : forest.outerMostBindings) { 130 // initially, the inherited context is itself 131 buildTargetNodeMap(jaxbBindings, jaxbBindings, null, targetNodes, scd); 132 } 133 134 // 135 // then move them to their respective positions. 136 // 137 for (Element jaxbBindings : forest.outerMostBindings) { 138 move(jaxbBindings, targetNodes); 139 } 140 141 return scd; 142 } 143 144 /** 145 * Determines the target node of the "bindings" element 146 * by using the inherited target node, then put 147 * the result into the "result" map and the "scd" map. 148 * 149 * @param inheritedTarget 150 * The current target node. This always exists, even if 151 * the user starts specifying targets via SCD (in that case 152 * this inherited target is just not going to be used.) 153 * @param inheritedSCD 154 * If the ancestor {@code <bindings>} node specifies @scd to 155 * specify the target via SCD, then this parameter represents that context. 156 */ 157 private void buildTargetNodeMap( Element bindings, @NotNull Node inheritedTarget, 158 @Nullable SCDBasedBindingSet.Target inheritedSCD, 159 Map<Element,List<Node>> result, SCDBasedBindingSet scdResult ) { 160 // start by the inherited target 161 Node target = inheritedTarget; 162 ArrayList<Node> targetMultiple = null; 163 164 // validate this node ? 165 // validate(bindings); 166 167 boolean required = true; 168 boolean multiple = false; 169 170 if(bindings.getAttribute("required") != null) { 171 String requiredAttr = bindings.getAttribute("required"); 172 173 if(requiredAttr.equals("no") || requiredAttr.equals("false") || requiredAttr.equals("0")) 174 required = false; 175 } 176 177 if(bindings.getAttribute("multiple") != null) { 178 String requiredAttr = bindings.getAttribute("multiple"); 179 180 if(requiredAttr.equals("yes") || requiredAttr.equals("true") || requiredAttr.equals("1")) 181 multiple = true; 182 } 183 184 185 // look for @schemaLocation 186 if( bindings.getAttributeNode("schemaLocation")!=null ) { 187 String schemaLocation = bindings.getAttribute("schemaLocation"); 188 189 // enhancement - schemaLocation="*" = bind to all schemas.. 190 if(schemaLocation.equals("*")) { 191 for(String systemId : forest.listSystemIDs()) { 192 if (result.get(bindings) == null) 193 result.put(bindings, new ArrayList<Node>()); 194 result.get(bindings).add(forest.get(systemId).getDocumentElement()); 195 196 Element[] children = DOMUtils.getChildElements(bindings, Const.JAXB_NSURI, "bindings"); 197 for (Element value : children) 198 buildTargetNodeMap(value, forest.get(systemId).getDocumentElement(), inheritedSCD, result, scdResult); 199 } 200 return; 201 } else { 202 try { 203 // TODO: use the URI class 204 // TODO: honor xml:base 205 URL loc = new URL( 206 new URL(forest.getSystemId(bindings.getOwnerDocument())), schemaLocation 207 ); 208 schemaLocation = loc.toExternalForm(); 209 target = forest.get(schemaLocation); 210 if ((target == null) && (loc.getProtocol().startsWith("file"))) { 211 File f = new File(loc.getFile()); 212 schemaLocation = new File(f.getCanonicalPath()).toURI().toString(); 213 } 214 } catch( MalformedURLException e ) { 215 } catch( IOException e ) { 216 Logger.getLogger(Internalizer.class.getName()).log(Level.FINEST, e.getLocalizedMessage()); 217 } 218 219 target = forest.get(schemaLocation); 220 if(target==null) { 221 reportError( bindings, 222 Messages.format(Messages.ERR_INCORRECT_SCHEMA_REFERENCE, 223 schemaLocation, 224 EditDistance.findNearest(schemaLocation,forest.listSystemIDs()))); 225 226 return; // abort processing this <jaxb:bindings> 227 } 228 229 target = ((Document)target).getDocumentElement(); 230 } 231 } 232 233 // look for @node 234 if( bindings.getAttributeNode("node")!=null ) { 235 String nodeXPath = bindings.getAttribute("node"); 236 237 // evaluate this XPath 238 NodeList nlst; 239 try { 240 xpath.setNamespaceContext(new NamespaceContextImpl(bindings)); 241 nlst = (NodeList)xpath.evaluate(nodeXPath,target,XPathConstants.NODESET); 242 } catch (XPathExpressionException e) { 243 if(required) { 244 reportError( bindings, 245 Messages.format(Messages.ERR_XPATH_EVAL,e.getMessage()), e ); 246 return; // abort processing this <jaxb:bindings> 247 } else { 248 return; 249 } 250 } 251 252 if( nlst.getLength()==0 ) { 253 if(required) 254 reportError( bindings, 255 Messages.format(Messages.NO_XPATH_EVAL_TO_NO_TARGET, nodeXPath) ); 256 return; // abort 257 } 258 259 if( nlst.getLength()!=1 ) { 260 if(!multiple) { 261 reportError( bindings, 262 Messages.format(Messages.NO_XPATH_EVAL_TOO_MANY_TARGETS, nodeXPath,nlst.getLength()) ); 263 264 return; // abort 265 } else { 266 if(targetMultiple == null) targetMultiple = new ArrayList<Node>(); 267 for(int i = 0; i < nlst.getLength(); i++) { 268 targetMultiple.add(nlst.item(i)); 269 } 270 } 271 } 272 273 // check 274 if(!multiple || nlst.getLength() == 1) { 275 Node rnode = nlst.item(0); 276 if (!(rnode instanceof Element)) { 277 reportError(bindings, 278 Messages.format(Messages.NO_XPATH_EVAL_TO_NON_ELEMENT, nodeXPath)); 279 return; // abort 280 } 281 282 if (!forest.logic.checkIfValidTargetNode(forest, bindings, (Element) rnode)) { 283 reportError(bindings, 284 Messages.format(Messages.XPATH_EVAL_TO_NON_SCHEMA_ELEMENT, 285 nodeXPath, rnode.getNodeName())); 286 return; // abort 287 } 288 289 target = rnode; 290 } else { 291 for(Node rnode : targetMultiple) { 292 if (!(rnode instanceof Element)) { 293 reportError(bindings, 294 Messages.format(Messages.NO_XPATH_EVAL_TO_NON_ELEMENT, nodeXPath)); 295 return; // abort 296 } 297 298 if (!forest.logic.checkIfValidTargetNode(forest, bindings, (Element) rnode)) { 299 reportError(bindings, 300 Messages.format(Messages.XPATH_EVAL_TO_NON_SCHEMA_ELEMENT, 301 nodeXPath, rnode.getNodeName())); 302 return; // abort 303 } 304 } 305 } 306 } 307 308 // look for @scd 309 if( bindings.getAttributeNode("scd")!=null ) { 310 String scdPath = bindings.getAttribute("scd"); 311 if(!enableSCD) { 312 // SCD selector was found, but it's not activated. report an error 313 // but recover by handling it anyway. this also avoids repeated error messages. 314 reportError(bindings, 315 Messages.format(Messages.SCD_NOT_ENABLED)); 316 enableSCD = true; 317 } 318 319 try { 320 inheritedSCD = scdResult.createNewTarget( inheritedSCD, bindings, 321 SCD.create(scdPath, new NamespaceContextImpl(bindings)) ); 322 } catch (ParseException e) { 323 reportError( bindings, Messages.format(Messages.ERR_SCD_EVAL,e.getMessage()),e ); 324 return; // abort processing this bindings 325 } 326 } 327 328 // update the result map 329 if (inheritedSCD != null) { 330 inheritedSCD.addBinidng(bindings); 331 } else if (!multiple || targetMultiple == null) { 332 if (result.get(bindings) == null) 333 result.put(bindings, new ArrayList<Node>()); 334 result.get(bindings).add(target); 335 } else { 336 for (Node rnode : targetMultiple) { 337 if (result.get(bindings) == null) 338 result.put(bindings, new ArrayList<Node>()); 339 340 result.get(bindings).add(rnode); 341 } 342 343 } 344 345 346 // look for child <jaxb:bindings> and process them recursively 347 Element[] children = DOMUtils.getChildElements( bindings, Const.JAXB_NSURI, "bindings" ); 348 for (Element value : children) 349 if(!multiple || targetMultiple == null) 350 buildTargetNodeMap(value, target, inheritedSCD, result, scdResult); 351 else { 352 for(Node rnode : targetMultiple) { 353 buildTargetNodeMap(value, rnode, inheritedSCD, result, scdResult); 354 } 355 } 356 } 357 358 /** 359 * Moves JAXB customizations under their respective target nodes. 360 */ 361 private void move(Element bindings, Map<Element, List<Node>> targetNodes) { 362 List<Node> nodelist = targetNodes.get(bindings); 363 364 if(nodelist == null) { 365 return; // abort 366 } 367 368 for (Node target : nodelist) { 369 if (target == null) // this must be the result of an error on the external binding. 370 // recover from the error by ignoring this node 371 { 372 return; 373 } 374 375 for (Element item : DOMUtils.getChildElements(bindings)) { 376 String localName = item.getLocalName(); 377 378 if ("bindings".equals(localName)) { 379 // process child <jaxb:bindings> recursively 380 move(item, targetNodes); 381 } else if ("globalBindings".equals(localName)) { 382 // <jaxb:globalBindings> always go to the root of document. 383 Element root = forest.getOneDocument().getDocumentElement(); 384 if (root.getNamespaceURI().equals(WSDL_NS)) { 385 NodeList elements = root.getElementsByTagNameNS(XMLConstants.W3C_XML_SCHEMA_NS_URI, "schema"); 386 if ((elements == null) || (elements.getLength() < 1)) { 387 reportError(item, Messages.format(Messages.ORPHANED_CUSTOMIZATION, item.getNodeName())); 388 return; 389 } else { 390 moveUnder(item, (Element)elements.item(0)); 391 } 392 } else { 393 moveUnder(item, root); 394 } 395 } else { 396 if (!(target instanceof Element)) { 397 reportError(item, 398 Messages.format(Messages.CONTEXT_NODE_IS_NOT_ELEMENT)); 399 return; // abort 400 } 401 402 if (!forest.logic.checkIfValidTargetNode(forest, item, (Element) target)) { 403 reportError(item, 404 Messages.format(Messages.ORPHANED_CUSTOMIZATION, item.getNodeName())); 405 return; // abort 406 } 407 408 // move this node under the target 409 moveUnder(item, (Element) target); 410 } 411 } 412 } 413 } 414 415 /** 416 * Moves the "decl" node under the "target" node. 417 * 418 * @param decl 419 * A JAXB customization element (e.g., {@code <jaxb:class>}) 420 * 421 * @param target 422 * XML Schema element under which the declaration should move. 423 * For example, {@code <xs:element>} 424 */ 425 private void moveUnder( Element decl, Element target ) { 426 Element realTarget = forest.logic.refineTarget(target); 427 428 declExtensionNamespace( decl, target ); 429 430 // copy in-scope namespace declarations of the decl node 431 // to the decl node itself so that this move won't change 432 // the in-scope namespace bindings. 433 Element p = decl; 434 Set<String> inscopes = new HashSet<String>(); 435 while(true) { 436 NamedNodeMap atts = p.getAttributes(); 437 for( int i=0; i<atts.getLength(); i++ ) { 438 Attr a = (Attr)atts.item(i); 439 if( Const.XMLNS_URI.equals(a.getNamespaceURI()) ) { 440 String prefix; 441 if( a.getName().indexOf(':')==-1 ) prefix = ""; 442 else prefix = a.getLocalName(); 443 444 if( inscopes.add(prefix) && p!=decl ) { 445 // if this is the first time we see this namespace bindings, 446 // copy the declaration. 447 // if p==decl, there's no need to. Note that 448 // we want to add prefix to inscopes even if p==Decl 449 450 decl.setAttributeNodeNS( (Attr)a.cloneNode(true) ); 451 } 452 } 453 } 454 455 if( p.getParentNode() instanceof Document ) 456 break; 457 458 p = (Element)p.getParentNode(); 459 } 460 461 if( !inscopes.contains("") ) { 462 // if the default namespace was undeclared in the context of decl, 463 // it must be explicitly set to "" since the new environment might 464 // have a different default namespace URI. 465 decl.setAttributeNS(Const.XMLNS_URI,"xmlns",""); 466 } 467 468 469 // finally move the declaration to the target node. 470 if( realTarget.getOwnerDocument()!=decl.getOwnerDocument() ) { 471 // if they belong to different DOM documents, we need to clone them 472 Element original = decl; 473 decl = (Element)realTarget.getOwnerDocument().importNode(decl,true); 474 475 // this effectively clones a ndoe,, so we need to copy locators. 476 copyLocators( original, decl ); 477 } 478 479 realTarget.appendChild( decl ); 480 } 481 482 /** 483 * Recursively visits sub-elements and declare all used namespaces. 484 * TODO: the fact that we recognize all namespaces in the extension 485 * is a bad design. 486 */ 487 private void declExtensionNamespace(Element decl, Element target) { 488 // if this comes from external namespaces, add the namespace to 489 // @extensionBindingPrefixes. 490 if( !Const.JAXB_NSURI.equals(decl.getNamespaceURI()) ) 491 declareExtensionNamespace( target, decl.getNamespaceURI() ); 492 493 NodeList lst = decl.getChildNodes(); 494 for( int i=0; i<lst.getLength(); i++ ) { 495 Node n = lst.item(i); 496 if( n instanceof Element ) 497 declExtensionNamespace( (Element)n, target ); 498 } 499 } 500 501 502 /** Attribute name. */ 503 private static final String EXTENSION_PREFIXES = "extensionBindingPrefixes"; 504 505 /** 506 * Adds the specified namespace URI to the jaxb:extensionBindingPrefixes 507 * attribute of the target document. 508 */ 509 private void declareExtensionNamespace( Element target, String nsUri ) { 510 // look for the attribute 511 Element root = target.getOwnerDocument().getDocumentElement(); 512 Attr att = root.getAttributeNodeNS(Const.JAXB_NSURI,EXTENSION_PREFIXES); 513 if( att==null ) { 514 String jaxbPrefix = allocatePrefix(root,Const.JAXB_NSURI); 515 // no such attribute. Create one. 516 att = target.getOwnerDocument().createAttributeNS( 517 Const.JAXB_NSURI,jaxbPrefix+':'+EXTENSION_PREFIXES); 518 root.setAttributeNodeNS(att); 519 } 520 521 String prefix = allocatePrefix(root,nsUri); 522 if( att.getValue().indexOf(prefix)==-1 ) 523 // avoid redeclaring the same namespace twice. 524 att.setValue( att.getValue()+' '+prefix); 525 } 526 527 /** 528 * Declares a new prefix on the given element and associates it 529 * with the specified namespace URI. 530 * <p> 531 * Note that this method doesn't use the default namespace 532 * even if it can. 533 */ 534 private String allocatePrefix( Element e, String nsUri ) { 535 // look for existing namespaces. 536 NamedNodeMap atts = e.getAttributes(); 537 for( int i=0; i<atts.getLength(); i++ ) { 538 Attr a = (Attr)atts.item(i); 539 if( Const.XMLNS_URI.equals(a.getNamespaceURI()) ) { 540 if( a.getName().indexOf(':')==-1 ) continue; 541 542 if( a.getValue().equals(nsUri) ) 543 return a.getLocalName(); // found one 544 } 545 } 546 547 // none found. allocate new. 548 while(true) { 549 String prefix = "p"+(int)(Math.random()*1000000)+'_'; 550 if(e.getAttributeNodeNS(Const.XMLNS_URI,prefix)!=null) 551 continue; // this prefix is already allocated. 552 553 e.setAttributeNS(Const.XMLNS_URI,"xmlns:"+prefix,nsUri); 554 return prefix; 555 } 556 } 557 558 559 /** 560 * Copies location information attached to the "src" node to the "dst" node. 561 */ 562 private void copyLocators( Element src, Element dst ) { 563 forest.locatorTable.storeStartLocation( 564 dst, forest.locatorTable.getStartLocation(src) ); 565 forest.locatorTable.storeEndLocation( 566 dst, forest.locatorTable.getEndLocation(src) ); 567 568 // recursively process child elements 569 Element[] srcChilds = DOMUtils.getChildElements(src); 570 Element[] dstChilds = DOMUtils.getChildElements(dst); 571 572 for( int i=0; i<srcChilds.length; i++ ) 573 copyLocators( srcChilds[i], dstChilds[i] ); 574 } 575 576 577 private void reportError( Element errorSource, String formattedMsg ) { 578 reportError( errorSource, formattedMsg, null ); 579 } 580 581 private void reportError( Element errorSource, 582 String formattedMsg, Exception nestedException ) { 583 584 SAXParseException e = new SAXParseException2( formattedMsg, 585 forest.locatorTable.getStartLocation(errorSource), 586 nestedException ); 587 errorHandler.error(e); 588 } 589 }