Permalink
Name already in use
A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
match/app/src/Lib/Match/ResultManager.php
Go to fileThis commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
343 lines (288 sloc)
10 KB
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* COmanage Match Result Manager | |
* | |
* Result Manager knows about both the wire format and database format in order | |
* to convert between them. | |
* | |
* Portions licensed to the University Corporation for Advanced Internet | |
* Development, Inc. ("UCAID") under one or more contributor license agreements. | |
* See the NOTICE file distributed with this work for additional information | |
* regarding copyright ownership. | |
* | |
* UCAID licenses this file to you under the Apache License, Version 2.0 | |
* (the "License"); you may not use this file except in compliance with the | |
* License. You may obtain a copy of the License at: | |
* | |
* http://www.apache.org/licenses/LICENSE-2.0 | |
* | |
* Unless required by applicable law or agreed to in writing, software | |
* distributed under the License is distributed on an "AS IS" BASIS, | |
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
* See the License for the specific language governing permissions and | |
* limitations under the License. | |
* | |
* @link http://www.internet2.edu/comanage COmanage Project | |
* @package match | |
* @since COmanage Match v1.0.0 | |
* @license Apache License, Version 2.0 (http://www.apache.org/licenses/LICENSE-2.0) | |
*/ | |
declare(strict_types = 1); | |
namespace App\Lib\Match; | |
use \App\Lib\Enum\ConfidenceModeEnum; | |
class ResultManager { | |
// Results are keyed on referenceId and rowId (so we can track multiple SOR data | |
// for the same referenceId). Attributes are stored in the same format as AttributeManager | |
// to faciliate wire representation. | |
protected $results = []; | |
// Track which rule created which results. This will only be populated for rules | |
// that generated results. | |
protected $resultRules = []; | |
// Keep the raw results as well, to facilitate UI representation. | |
protected $rawResults = []; | |
// "canonical" or "potential" | |
protected $confidenceMode = null; | |
protected $attrconfig = []; | |
// We track which rules ran successfully as a way of validating the inbound request. | |
// This includes rules that did not generate any results. | |
protected $successfulRules = []; | |
/** | |
* Add a Matchgrid result. | |
* | |
* @since COmanage Match v1.0.0 | |
* @param string $referenceId Reference ID | |
* @param array $attributes Array of matchgrid attributes | |
* @param string $rule Rule that generated this result | |
* @throws RuntimeException | |
*/ | |
public function add(array $attributes, string $rule=null) { | |
if($rule) { | |
$this->successfulRules[] = $rule; | |
} | |
if(empty($attributes)) { | |
return; | |
} | |
$referenceId = null; | |
$rowId = null; | |
$parsed = []; | |
foreach($attributes as $name => $value) { | |
// We treat all names as case insensitive since the database column name | |
// is case insensitive. | |
$lname = strtolower($name); | |
if($lname == 'id') { | |
$rowId = $value; | |
} elseif($lname == 'referenceid') { | |
$referenceId = $value; | |
} else { | |
// Store keyed on the API name, not the database name | |
if(!empty($this->attrconfig[$lname])) { | |
$parsed[ $this->attrconfig[$lname] ] = $value; | |
} else { | |
// eg: sor | |
$parsed[$lname] = $value; | |
} | |
} | |
} | |
// We might get a null $referenceId (pending matches), but we shouldn't get | |
// a null $rowId. | |
if(!$rowId) { | |
throw new \RuntimeException(__('match.er.format')); | |
} | |
$this->results[$referenceId][$rowId] = $parsed; | |
$this->resultRules[$rowId] = $rule; | |
$this->rawResults[$rowId] = $attributes; | |
} | |
/** | |
* Obtain the number of results. | |
* | |
* @since COmanage Match v1.0.0 | |
* @return int Result count | |
*/ | |
public function count() { | |
return count($this->results); | |
} | |
/** | |
* Filter Metadata attributes in preparation for the generation of a Match Result. | |
* | |
* @since COmanage Match v1.0.0 | |
* @param array $parsed Record attributes | |
* @param string $referenceId Reference ID, if known | |
* @return array Attributes parsed into 'meta' and 'sorAttributes' | |
*/ | |
protected function filterMetadata($parsed, $referenceId=null) { | |
$ret = array(); | |
if($referenceId) { | |
$ret['meta']['referenceId'] = $referenceId; | |
} | |
foreach(array_keys($parsed) as $attr) { | |
switch($attr) { | |
case 'matchRequest': | |
// Force to a string as per the spec (internally we use an int) | |
$ret['meta'][$attr] = (string)$parsed[$attr]; | |
break; | |
case 'request_time': | |
case 'resolution_time': | |
// Timestamps, format and inflect name | |
$ret['meta'][\Cake\Utility\Inflector::variable($attr)] = date("Y-m-d\TH:i:s\Z", | |
strtotime($parsed[$attr])); | |
break; | |
case 'sor': | |
$ret['meta']['sorLabel'] = $parsed[$attr]; | |
break; | |
default: | |
// Any other attribute is not metadata, so just copy to sorAttributes | |
$ret['sorAttributes'][$attr] = $parsed[$attr]; | |
break; | |
} | |
} | |
// If an SOR ID is present in an Identifier, copy it to the metadata | |
// (but leave the original where it was). | |
$sorId = \Cake\Utility\Hash::extract($parsed, "identifiers.{n}[type=sor].identifier"); | |
if(!empty($sorId)) { | |
$ret['meta']['sorId'] = $sorId[0]; | |
} | |
return $ret; | |
} | |
/** | |
* Get the Confidence Mode for this set of results. | |
* | |
* @since COmanage Match v1.0.0 | |
* @return ConfidenceModeEnum Confidence Mode | |
*/ | |
public function getConfidenceMode() { | |
return $this->confidenceMode; | |
} | |
/** | |
* Obtain the raw array of results. | |
* | |
* @since COmanage Match v1.0.0 | |
* @return Array Results | |
*/ | |
public function getRawResults() { | |
return $this->rawResults; | |
} | |
/** | |
* Obtain an array of reference IDs in the result set. | |
* | |
* @since COmanage Match v1.0.0 | |
* @return Array Reference IDs | |
*/ | |
public function getReferenceIds() { | |
return array_keys($this->results); | |
} | |
/** | |
* Obtain the results in an array format suitable for converting to JSON. | |
* | |
* @since COmanage Match v1.0.0 | |
* @param string $mode "search" (Search Reference ID), "current" (Request Current Values), or "pending" (Request Pending/Resolved Matches) | |
* @return array JSON-ready results | |
*/ | |
public function getResultsForJson($mode="search") { | |
$ret = []; | |
foreach($this->results as $referenceId => $sorRow) { | |
// Note $candidate is not used by mode=pending | |
$candidate = [ | |
'referenceId' => $referenceId, | |
'sorRecords' => [] | |
]; | |
foreach($sorRow as $rowId => $attrs) { | |
$parsed = ['matchRequest' => $rowId]; | |
// We'll sort complex attributes by type before converting them | |
// to the required format | |
$complex = []; | |
foreach($attrs as $a => $v) { | |
// Should we skip empty values? We do for now... | |
if(empty($v)) | |
continue; | |
if(strstr($a, ':')) { | |
// eg: names:given | |
$super = explode(':', $a, 2); | |
// eg: given/official | |
$sub = explode('/', $super[1], 2); | |
if(!empty($sub[1])) { | |
$complex[ $super[0] ][ $sub[1] ][ $sub[0] ] = $v; | |
} | |
} elseif($a == 'sorid') { | |
// Special case | |
$parsed['identifiers'][] = [ | |
"type" => "sor", | |
"identifier" => $v | |
]; | |
} else { | |
// eg: dateOfBirth | |
$parsed[$a] = $v; | |
} | |
} | |
// Convert complex attributes into the correct wire format | |
foreach(array_keys($complex) as $attr) { | |
foreach($complex[$attr] as $t => $v) { | |
$v['type'] = $t; | |
$parsed[$attr][] = $v; | |
} | |
} | |
if($mode == 'current') { | |
// There should only be one entry, so return it directly | |
return $this->filterMetadata($parsed, $referenceId); | |
} elseif($mode == 'pending') { | |
$ret[$rowId] = $this->filterMetadata($parsed, $referenceId); | |
} else { | |
// Search Reference ID | |
$candidate['sorRecords'][] = $this->filterMetadata($parsed, $referenceId); | |
} | |
} | |
if($mode != 'pending') { | |
$ret[] = $candidate; | |
} | |
} | |
return $ret; | |
} | |
/** | |
* Get the name of the rule that generated the result for the specified row ID. | |
* | |
* @since COmanage Match v1.2.0 | |
* @param int $rowId Row to return rule for | |
* @return string Rule name (if found) or false | |
*/ | |
public function getRuleForResult(int $rowId) { | |
return $this->resultRules[$rowId] ?? false; | |
} | |
/** | |
* Obtain an array of successfully executed rules as reported to the ResultManager. | |
* Note that successful rules may have generated no matches. | |
* | |
* @since COmanage Match v1.0.0 | |
* @return Array Reference IDs | |
*/ | |
public function getSuccessfulRules() { | |
return $this->successfulRules; | |
} | |
/** | |
* Set the Confidence Mode for this set of results. | |
* | |
* @since COmanage Match v1.0.0 | |
* @param ConfidenceModeEnum $mode Confidence Mode | |
*/ | |
public function setConfidenceMode(string $mode) { | |
$this->confidenceMode = $mode; | |
} | |
/** | |
* Set the attribute configuration. | |
* | |
* @since COmanage Match v1.0.0 | |
* @param array $config Array of Attributes (and nested AttributeGroups) | |
*/ | |
public function setConfig(array $config) { | |
// Initially we only need the database name -> api name mapping. | |
foreach($config as $attr) { | |
$apiname = $attr->api_name; | |
if(!empty($attr->attribute_group->name)) { | |
// Append the Attribute Group name to the end of the name, since (for | |
// now, anyway) attribute groups are just types within a complex attribute. | |
$apiname .= '/' . $attr->attribute_group->name; | |
} | |
// We treat attributes names as case insensitive since the database column | |
// is case insensitive. | |
$this->attrconfig[ strtolower($attr->name) ] = $apiname; | |
} | |
} | |
} |