#
source:
TI12-security/trunk/python/ndg.security.common/ndg/security/common/authz/xacml/cond.py
@
5375

Subversion URL: http://proj.badc.rl.ac.uk/svn/ndg/TI12-security/trunk/python/ndg.security.common/ndg/security/common/authz/xacml/cond.py@5375
Revision 5375, 23.1 KB checked in by pjkersha, 12 years ago (diff) |
---|

Line | |
---|---|

1 | """XACML cond module contains condition function classes |

2 | |

3 | NERC DataGrid Project |

4 | """ |

5 | __author__ = "P J Kershaw" |

6 | __date__ = "02/04/09" |

7 | __copyright__ = "(C) 2009 Science and Technology Facilities Council" |

8 | __contact__ = "Philip.Kershaw@stfc.ac.uk" |

9 | __license__ = "BSD - see LICENSE file in top-level directory" |

10 | __contact__ = "Philip.Kershaw@stfc.ac.uk" |

11 | __revision__ = "$Id$" |

12 | import logging |

13 | log = logging.getLogger(__name__) |

14 | |

15 | from ndg.security.common.authz.xacml.cond.eval import Evaluatable |

16 | from ndg.security.common.authz.xacml.attr import AnyURIAttribute, \ |

17 | Base64BinaryAttribute, BooleanAttribute, DateAttribute, DateTimeAttribute,\ |

18 | DayTimeDurationEqual, DoubleAttribute, HexBinaryAttribute, \ |

19 | IntegerAttribute, RFC822NameAttribute, StringAttribute, TimeAttribute, \ |

20 | X500NameAttribute, YearMonthDurationAttribute |

21 | |

22 | |

23 | class Apply(Evaluatable): |

24 | '''Represents the XACML ApplyType and ConditionType XML types.''' |

25 | |

26 | def __init__(self, function, evals, bagFunction=None, isCondition=False): |

27 | '''Constructs an Apply object. Throws an |

28 | IllegalArgumentException if the given parameter list |

29 | isn't valid for the given function. |

30 | |

31 | @param function the Function to use in evaluating the elements in the |

32 | apply |

33 | @param evals the contents of the apply which will be the parameters |

34 | to the function, each of which is an Evaluatable |

35 | @param bagFunction the higher-order function to use |

36 | @param isCondition Rrue if this Apply is a Condition, False otherwise |

37 | ''' |

38 | |

39 | # check that the given inputs work for the function |

40 | inputs = evals |

41 | if bagFunction is not None: |

42 | inputs = [bagFunction] |

43 | inputs += evals |

44 | |

45 | function.checkInputs(inputs) |

46 | |

47 | # if everything checks out, then store the inputs |

48 | self.function = function |

49 | self.evals = tuple(evals) |

50 | self.bagFunction = bagFunction |

51 | self.isCondition = isCondition |

52 | |

53 | |

54 | @staticmethod |

55 | def getConditionInstance(root, xpathVersion): |

56 | '''Returns an instance of an Apply based on the given DOM |

57 | root node. This will actually return a special kind of |

58 | Apply, namely an XML ConditionType, which is the root |

59 | of the condition logic in a RuleType. A ConditionType is the same |

60 | as an ApplyType except that it must use a FunctionId that returns |

61 | a boolean value. |

62 | |

63 | @param root the DOM root of a ConditionType XML type |

64 | @param xpathVersion the XPath version to use in any selectors or XPath |

65 | functions, or null if this is unspecified (ie, not |

66 | supplied in the defaults section of the policy) |

67 | |

68 | ''' |

69 | raise NotImplementedError() |

70 | |

71 | def getInstance(self, |

72 | root, |

73 | factory=None, |

74 | isCondition=False, |

75 | xpathVersion=None): |

76 | '''Returns an instance of Apply based on the given DOM root. |

77 | |

78 | @param root the DOM root of an ApplyType XML type |

79 | @param xpathVersion the XPath version to use in any selectors or XPath |

80 | functions, or null if this is unspecified (ie, not |

81 | supplied in the defaults section of the policy)''' |

82 | |

83 | raise NotImplementedError() |

84 | |

85 | @staticmethod |

86 | def getFunction(root, version, factory): |

87 | '''Helper method that tries to get a function instance''' |

88 | raise NotImplementedError() |

89 | |

90 | def getFunction(self): |

91 | '''Returns the Function used by this Apply. |

92 | |

93 | @return the Function''' |

94 | return function |

95 | |

96 | def getChildren(self): |

97 | '''Returns the List of children for this Apply. |

98 | The List contains Evaluatables. The list is |

99 | unmodifiable, and may be empty. |

100 | |

101 | @return a List of Evaluatables''' |

102 | return self.evals |

103 | |

104 | def getHigherOrderFunction(self): |

105 | '''Returns the higher order bag function used by this Apply |

106 | if it exists, or null if no higher order function is used. |

107 | |

108 | @return the higher order Function or null''' |

109 | return self.bagFunction |

110 | |

111 | def isCondition(self): |

112 | '''Returns whether or not this ApplyType is actually a ConditionType. |

113 | |

114 | @return whether or not this represents a ConditionType''' |

115 | return isCondition |

116 | |

117 | def evaluate(self, context): |

118 | '''Evaluates the apply object using the given function. This will in |

119 | turn call evaluate on all the given parameters, some of which may be |

120 | other Apply objects. |

121 | |

122 | @param context the representation of the request |

123 | |

124 | @return the result of trying to evaluate this apply object''' |

125 | parameters = self.evals |

126 | |

127 | # see if there is a higher-order function in here |

128 | if bagFunction != None: |

129 | # this is a special case, so we setup the parameters, starting |

130 | # with the function |

131 | parameters = [bagFunction] |

132 | |

133 | # now we evaluate all the parameters, returning INDETERMINATE |

134 | # if that's what any of them return, and otherwise tracking |

135 | # all the AttributeValues that get returned |

136 | for eval in self.evals: |

137 | result = eval.evaluate(context) |

138 | |

139 | # in a higher-order case, if anything is INDETERMINATE, then |

140 | # we stop right away |

141 | if result.indeterminate(): |

142 | return result |

143 | |

144 | parameters.add(result.getAttributeValue()) |

145 | |

146 | # now we can call the base function |

147 | return function.evaluate(parameters, context) |

148 | |

149 | def getType(self): |

150 | '''Returns the type of attribute that this object will return on a call |

151 | to evaluate. In practice, this will always be the same as |

152 | the result of calling getReturnType on the function used |

153 | by this object. |

154 | |

155 | @return the type returned by evaluate''' |

156 | return self.function.getReturnType() |

157 | |

158 | def evaluatesToBag(self): |

159 | '''Returns whether or not the Function will return a bag |

160 | of values on evaluation. |

161 | |

162 | @return true if evaluation will return a bag of values, false otherwise |

163 | ''' |

164 | return self.function.returnsBag() |

165 | |

166 | def encode(self, output, indenter): |

167 | '''Encodes this Apply into its XML representation and |

168 | writes this encoding to the given OutputStream with |

169 | indentation. |

170 | |

171 | @param output a stream into which the XML-encoded data is written |

172 | @param indenter an object that creates indentation strings''' |

173 | raise NotImplementedError() |

174 | |

175 | class Function(object): |

176 | '''Interface that all functions in the system must implement.''' |

177 | |

178 | def evaluate(self, inputs, context): |

179 | '''Evaluates the Function using the given inputs. |

180 | The List contains Evaluatables which are all |

181 | of the correct type if the Function has been created as |

182 | part of an Apply or TargetMatch, but which |

183 | may otherwise be invalid. Each parameter should be evaluated by the |

184 | Function, unless this is a higher-order function (in |

185 | which case the Apply has already evaluated the inputs |

186 | to check for any INDETERMINATE conditions), or the Function |

187 | doesn't need to evaluate all inputs to determine a result (as in the |

188 | case of the or function). The order of the List is |

189 | significant, so a Function should have a very good reason |

190 | if it wants to evaluate the inputs in a different order. |

191 | <p> |

192 | Note that if this is a higher-order function, like any-of, then |

193 | the first argument in the List will actually be a Function |

194 | object representing the function to apply to some bag. In this case, |

195 | the second and any subsequent entries in the list are |

196 | AttributeValue objects (no INDETERMINATE values are |

197 | allowed, so the function is not given the option of dealing with |

198 | attributes that cannot be resolved). A function needs to know if it's |

199 | a higher-order function, and therefore whether or not to look for |

200 | this case. Also, a higher-order function is responsible for checking |

201 | that the inputs that it will pass to the Function |

202 | provided as the first parameter are valid, ie. it must do a |

203 | checkInputs on its sub-function when |

204 | checkInputs is called on the higher-order function. |

205 | |

206 | @param inputs the List of inputs for the function |

207 | @param context the representation of the request |

208 | |

209 | @return a result containing the AttributeValue computed |

210 | when evaluating the function, or Status |

211 | specifying some error condition''' |

212 | raise NotImplementedError() |

213 | |

214 | |

215 | def getIdentifier(self): |

216 | '''Returns the identifier of this function as known by the factories. |

217 | In the case of the standard XACML functions, this will be one of the |

218 | URIs defined in the standard namespace. This function must always |

219 | return the complete namespace and identifier of this function. |

220 | |

221 | @return the function's identifier''' |

222 | raise NotImplementedError() |

223 | |

224 | def getReturnType(self): |

225 | '''Provides the type of AttributeValue that this function |

226 | returns from evaluate in a successful evaluation. |

227 | |

228 | @return the type returned by this function |

229 | ''' |

230 | raise NotImplementedError() |

231 | |

232 | def returnsBag(self): |

233 | '''Tells whether this function will return a bag of values or just a |

234 | single value. |

235 | |

236 | @return true if evaluation will return a bag, false otherwise''' |

237 | raise NotImplementedError() |

238 | |

239 | def checkInputs(self, inputs): |

240 | '''Checks that the given inputs are of the right types, in the right |

241 | order, and are the right number for this function to evaluate. If |

242 | the function cannot accept the inputs for evaluation, an |

243 | IllegalArgumentException is thrown. |

244 | |

245 | @param inputs a list of Evaluatables, with the first argument being a |

246 | Function if this is a higher-order function |

247 | |

248 | @throws TypeError if the inputs do match what the function accepts for |

249 | evaluation |

250 | ''' |

251 | raise NotImplementedError() |

252 | |

253 | def checkInputsNoBag(self, inputs): |

254 | '''Checks that the given inputs are of the right types, in the right |

255 | order, and are the right number for this function to evaluate. If |

256 | the function cannot accept the inputs for evaluation, an |

257 | IllegalArgumentException is thrown. Unlike the other |

258 | checkInput method in this interface, this assumes that |

259 | the parameters will never provide bags of values. This is useful if |

260 | you're considering a target function which has a designator or |

261 | selector in its input list, but which passes the values from the |

262 | derived bags one at a time to the function, so the function doesn't |

263 | have to deal with the bags that the selector or designator |

264 | generates. |

265 | |

266 | @param inputs a list of Evaluatables, with the first argument being a |

267 | Function if this is a higher-order function |

268 | |

269 | @throws TypeError if the inputs do match what the function accepts for |

270 | evaluation''' |

271 | raise NotImplementedError() |

272 | |

273 | |

274 | class FunctionBase(Function): |

275 | FUNCTION_NS = "urn:oasis:names:tc:xacml:1.0:function:" |

276 | supportedIdentifiers = () |

277 | |

278 | def __init__(self, |

279 | functionName, |

280 | functionId=None, |

281 | paramType=None, |

282 | paramIsBag=False, |

283 | numParams=0, |

284 | minParams=0, |

285 | returnType='', |

286 | returnsBag=False): |

287 | ''' |

288 | @param functionName the name of this function as used by the factory |

289 | and any XACML policies |

290 | @param functionId an optional identifier that can be used by your |

291 | code for convenience |

292 | @param paramTypes the type of each parameter, in order, required by |

293 | this function, as used by the factory and any XACML |

294 | documents |

295 | @param paramIsBag whether or not each parameter is actually a bag |

296 | of values |

297 | @param returnType the type returned by this function, as used by |

298 | the factory and any XACML documents |

299 | @param returnsBag whether or not this function returns a bag of values |

300 | ''' |

301 | |

302 | self.functionName = functionName |

303 | self.functionId = functionId |

304 | self.returnType = None |

305 | self.returnsBag = False |

306 | |

307 | self.singleType = True; |

308 | |

309 | self.paramType = paramType |

310 | self.paramIsBag = paramIsBag |

311 | self.numParams = numParams |

312 | self.minParams = minParams |

313 | |

314 | |

315 | def _setFunctionName(self, functionName): |

316 | if functionName not in self.__class__.supportedIdentifiers: |

317 | functionList = ', '.join(self.__class__.supportedIdentifiers) |

318 | raise TypeError("Function name [%s] is not on of the recognised " |

319 | "types: %s" % (functionName, functionList)) |

320 | self._functionName = functionName |

321 | |

322 | def _getFunctionName(self): |

323 | return getattr(self, '_functionName', None) |

324 | |

325 | functionName = property(fset=_setFunctionName, |

326 | fget=_getFunctionName) |

327 | |

328 | def checkInputs(self, inputs): |

329 | '''Checks that the given inputs are of the right types, in the right |

330 | order, and are the right number for this function to evaluate.''' |

331 | raise NotImplementedError() |

332 | |

333 | def checkInputsNoBag(self, inputs): |

334 | '''Checks that the given inputs are of the right types, in the right |

335 | order, and are the right number for this function to evaluate.''' |

336 | raise NotImplementedError() |

337 | |

338 | def evaluate(self, inputs, context): |

339 | '''Evaluates the Function using the given inputs.''' |

340 | raise NotImplementedError() |

341 | |

342 | def evalArgs(self, params, context, args): |

343 | '''Evaluates each of the parameters, in order, filling in the argument |

344 | array with the resulting values. If any error occurs, this method |

345 | returns the error, otherwise null is returned, signalling that |

346 | evaluation was successful for all inputs, and the resulting argument |

347 | list can be used. |

348 | |

349 | @param params a list of Evaluatable objects representing the parameters |

350 | to evaluate |

351 | @param context the representation of the request |

352 | @param args an array as long as the params list that will, on return, |

353 | contain the AttributeValues generated from evaluating all parameters |

354 | |

355 | @return None if no errors were encountered, otherwise |

356 | an EvaluationResult representing the error |

357 | ''' |

358 | index = 0 |

359 | |

360 | for eval in params: |

361 | # get and evaluate the next parameter |

362 | result = eval.evaluate(context) |

363 | |

364 | # If there was an error, pass it back... |

365 | if result.indeterminate(): |

366 | return result |

367 | |

368 | # ...otherwise save it and keep going |

369 | args[index] = result.getAttributeValue() |

370 | index += 1 |

371 | |

372 | return None |

373 | |

374 | # TODO: Condition classes - minimal implementation until opportunity to fully |

375 | # implement |

376 | class BagFunction(FunctionBase): |

377 | def __init__(self, *arg, **kw): |

378 | raise NotImplementedError() |

379 | |

380 | class SetFunction(FunctionBase): |

381 | def __init__(self, *arg, **kw): |

382 | raise NotImplementedError() |

383 | |

384 | class ConditionBagFunction(BagFunction): |

385 | def __init__(self, *arg, **kw): |

386 | raise NotImplementedError() |

387 | |

388 | class ConditionSetFunction(FunctionBase): |

389 | def __init__(self, *arg, **kw): |

390 | raise NotImplementedError() |

391 | |

392 | class HigherOrderFunction(Function): |

393 | def __init__(self, *arg, **kw): |

394 | raise NotImplementedError() |

395 | |

396 | # TODO: Function classes - minimal implementation until opportunity to fully |

397 | # implement |

398 | class LogicalFunction(FunctionBase): |

399 | |

400 | def __init__(self, *arg, **kw): |

401 | raise NotImplementedError() |

402 | |

403 | class NOfFunction(FunctionBase): |

404 | |

405 | def __init__(self, *arg, **kw): |

406 | raise NotImplementedError() |

407 | |

408 | class NotFunction(FunctionBase): |

409 | |

410 | def __init__(self, *arg, **kw): |

411 | raise NotImplementedError() |

412 | |

413 | class ComparisonFunction(FunctionBase): |

414 | |

415 | def __init__(self, *arg, **kw): |

416 | raise NotImplementedError() |

417 | |

418 | class MatchFunction(FunctionBase): |

419 | NAME_REGEXP_STRING_MATCH = FunctionBase.FUNCTION_NS + "regexp-string-match" |

420 | NAME_RFC822NAME_MATCH = FunctionBase.FUNCTION_NS + "rfc822Name-match" |

421 | NAME_X500NAME_MATCH = FunctionBase.FUNCTION_NS + "x500Name-match" |

422 | |

423 | supportedIdentifiers = (NAME_REGEXP_STRING_MATCH, |

424 | NAME_RFC822NAME_MATCH, |

425 | NAME_X500NAME_MATCH) |

426 | |

427 | lut = { |

428 | NAME_REGEXP_STRING_MATCH: 'regexpStringMatch', |

429 | NAME_RFC822NAME_MATCH: 'rfc822NameMatch', |

430 | NAME_X500NAME_MATCH: 'x500NameMatch' |

431 | } |

432 | |

433 | def __init__(self, functionName, **kw): |

434 | super(MatchFunction, self).__init__(functionName, **kw) |

435 | |

436 | def regexpStringMatch(self, regex, val): |

437 | return re.match(regex, val) is not None |

438 | |

439 | def rfc822NameMatch(self, *inputs): |

440 | raise NotImplementedError() |

441 | |

442 | def x500NameMatch(self, *inputs): |

443 | raise NotImplementedError() |

444 | |

445 | def evaluate(self, inputs, context): |

446 | matchFunction = getattr(self, MatchFunction.lut[self.functionName]) |

447 | match = matchFunction(self, *inputs) |

448 | if match: |

449 | return EvaluationResult(status=Status.STATUS_OK) |

450 | |

451 | |

452 | class EqualFunction(FunctionBase): |

453 | supportedIdentifiers = ( |

454 | FunctionBase.FUNCTION_NS + "anyURI-equal", |

455 | FunctionBase.FUNCTION_NS + "base64Binary-equal", |

456 | FunctionBase.FUNCTION_NS + "boolean-equal", |

457 | FunctionBase.FUNCTION_NS + "date-equal", |

458 | FunctionBase.FUNCTION_NS + "dateTime-equal", |

459 | FunctionBase.FUNCTION_NS + "dayTimeDuration-equal", |

460 | FunctionBase.FUNCTION_NS + "double-equal", |

461 | FunctionBase.FUNCTION_NS + "hexBinary-equal", |

462 | FunctionBase.FUNCTION_NS + "integer-equal", |

463 | FunctionBase.FUNCTION_NS + "rfc822Name-equal", |

464 | FunctionBase.FUNCTION_NS + "string-equal", |

465 | FunctionBase.FUNCTION_NS + "time-equal", |

466 | FunctionBase.FUNCTION_NS + "x500Name-equal", |

467 | FunctionBase.FUNCTION_NS + "yearMonthDuration-equal" |

468 | ) |

469 | |

470 | (NAME_ANYURI_EQUAL, |

471 | NAME_BASE64BINARY_EQUAL, |

472 | NAME_BOOLEAN_EQUAL, |

473 | NAME_DATE_EQUAL, |

474 | NAME_DATETIME_EQUAL, |

475 | NAME_DAYTIME_DURATION_EQUAL, |

476 | NAME_DOUBLE_EQUAL, |

477 | NAME_HEXBINARY_EQUAL, |

478 | NAME_INTEGER_EQUAL, |

479 | NAME_RFC822NAME_EQUAL, |

480 | NAME_STRING_EQUAL, |

481 | NAME_TIME_EQUAL, |

482 | NAME_X500NAME_EQUAL, |

483 | NAME_YEARMONTH_DURATION_EQUAL) = supportedIdentifiers |

484 | |

485 | lut = { |

486 | NAME_STRING_EQUAL: 'stringEqual' |

487 | } |

488 | |

489 | _attrClasses = ( |

490 | AnyURIAttribute, |

491 | Base64BinaryAttribute, |

492 | BooleanAttribute, |

493 | DateAttribute, |

494 | DateTimeAttribute, |

495 | DayTimeDurationEqual, |

496 | DoubleAttribute, |

497 | HexBinaryAttribute, |

498 | IntegerAttribute, |

499 | RFC822NameAttribute, |

500 | StringAttribute, |

501 | TimeAttribute, |

502 | X500NameAttribute, |

503 | YearMonthDurationAttribute |

504 | ) |

505 | |

506 | typeMap = dict([(i, j.identifier) for i,j in zip(supportedIdentifiers, |

507 | _attrClasses)]) |

508 | |

509 | def __init__(self, functionName, argumentType=None, **kw): |

510 | if kw.get('functionId') is None: |

511 | kw['functionId'] = functionName |

512 | |

513 | if kw.get('paramType') is None: |

514 | kw['paramType'] = EqualFunction._getArgumentType(functionName) |

515 | |

516 | super(EqualFunction, self).__init__(functionName, **kw) |

517 | |

518 | def evaluate(self, inputs, evaluationCtx): |

519 | function = EqualFunction.lut.get(self.functionName) |

520 | if function is None: |

521 | if self.functionName in supportedIdentifiers: |

522 | raise NotImplementedError("No implementation is available for " |

523 | "%s" % self.functionName) |

524 | else: |

525 | raise AttributeError('function name "%s" not recognised ' |

526 | 'for %s' % (self.functionName, |

527 | self.__class__.__name__)) |

528 | |

529 | return getattr(self, function)(inputs, evaluationCtx) |

530 | |

531 | def stringEqual(self, inputs, evaluationCtx): |

532 | result = self.evalArgs(inputs, context, argValues) |

533 | if result is not None: |

534 | return result |

535 | |

536 | return EvaluationResult(argValues[0] == argValues[1]) |

537 | |

538 | @classmethod |

539 | def _getArgumentType(cls, functionName): |

540 | argumentType = cls.typeMap.get(functionName) |

541 | if argumentType is None: |

542 | if functionName in cls.supportedIdentifiers: |

543 | raise NotImplementedError('No implementation is currently ' |

544 | 'available for "%s"' % functionName) |

545 | else: |

546 | raise TypeError("Not a standard function: %s" % functionName) |

547 | |

548 | return argumentType |

549 | |

550 | class AddFunction(FunctionBase): |

551 | |

552 | def __init__(self, *arg, **kw): |

553 | raise NotImplementedError() |

554 | |

555 | class SubtractFunction(FunctionBase): |

556 | |

557 | def __init__(self, *arg, **kw): |

558 | raise NotImplementedError() |

559 | |

560 | class MultiplyFunction(FunctionBase): |

561 | |

562 | def __init__(self, *arg, **kw): |

563 | raise NotImplementedError() |

564 | |

565 | class DivideFunction(FunctionBase): |

566 | |

567 | def __init__(self, *arg, **kw): |

568 | raise NotImplementedError() |

569 | |

570 | class ModFunction(FunctionBase): |

571 | |

572 | def __init__(self, *arg, **kw): |

573 | raise NotImplementedError() |

574 | |

575 | class AbsFunction(FunctionBase): |

576 | |

577 | def __init__(self, *arg, **kw): |

578 | raise NotImplementedError() |

579 | |

580 | class RoundFunction(FunctionBase): |

581 | |

582 | def __init__(self, *arg, **kw): |

583 | raise NotImplementedError() |

584 | |

585 | class FloorFunction(FunctionBase): |

586 | |

587 | def __init__(self, *arg, **kw): |

588 | raise NotImplementedError() |

589 | |

590 | class DateMathFunction(FunctionBase): |

591 | |

592 | def __init__(self, *arg, **kw): |

593 | raise NotImplementedError() |

594 | |

595 | class GeneralBagFunction(BagFunction): |

596 | |

597 | def __init__(self, *arg, **kw): |

598 | raise NotImplementedError() |

599 | |

600 | class NumericConvertFunction(FunctionBase): |

601 | |

602 | def __init__(self, *arg, **kw): |

603 | raise NotImplementedError() |

604 | |

605 | class StringNormalizeFunction(FunctionBase): |

606 | |

607 | def __init__(self, *arg, **kw): |

608 | raise NotImplementedError() |

609 | |

610 | class GeneralSetFunction(SetFunction): |

611 | |

612 | def __init__(self, *arg, **kw): |

613 | raise NotImplementedError() |

614 | |

615 | class MapFunction(Function): |

616 | supportedIdentifiers = () |

617 | NAME_MAP = FunctionBase.FUNCTION_NS + "map" |

618 | |

619 | def __init__(self, *arg, **kw): |

620 | raise NotImplementedError() |

621 | |

622 | @classmethod |

623 | def getInstance(cls, root): |

624 | raise NotImplementedError() |

625 | |

626 | class FunctionProxy(): |

627 | |

628 | def getInstance(self, root): |

629 | raise NotImplementedError() |

630 | |

631 | class MapFunctionProxy(FunctionProxy): |

632 | |

633 | def getInstance(self, root): |

634 | return MapFunction.getInstance(root) |

**Note:**See TracBrowser for help on using the repository browser.