00001 <?PHP
00002 #
00003 # FILE: MetadataSchema.php
00004 #
00005 # Part of the Collection Workflow Integration System (CWIS)
00006 # Copyright 2011 Edward Almasy and Internet Scout
00007 # http://scout.wisc.edu
00008 #
00009
00010 class MetadataSchema extends ItemFactory {
00011
00012 # ---- PUBLIC INTERFACE --------------------------------------------------
00013
00014 # types of field ordering
00015 const MDFORDER_DISPLAY = 1;
00016 const MDFORDER_EDITING = 2;
00017 const MDFORDER_ALPHABETICAL = 3;
00018
00019 # metadata field types
00020 # (must parallel MetadataFields.FieldType declaration in install/CreateTables.sql
00021 # and MetadataField::$FieldTypeDBEnums declaration below)
00022 const MDFTYPE_TEXT = 1;
00023 const MDFTYPE_PARAGRAPH = 2;
00024 const MDFTYPE_NUMBER = 4;
00025 const MDFTYPE_DATE = 8;
00026 const MDFTYPE_TIMESTAMP = 16;
00027 const MDFTYPE_FLAG = 32;
00028 const MDFTYPE_TREE = 64;
00029 const MDFTYPE_CONTROLLEDNAME = 128;
00030 const MDFTYPE_OPTION = 256;
00031 const MDFTYPE_USER = 512;
00032 const MDFTYPE_IMAGE = 1024;
00033 const MDFTYPE_FILE = 2048;
00034 const MDFTYPE_URL = 4096;
00035 const MDFTYPE_POINT = 8192;
00036
00037 # error status codes
00038 const MDFSTAT_OK = 1;
00039 const MDFSTAT_ERROR = 2;
00040 const MDFSTAT_DUPLICATENAME = 4;
00041 const MDFSTAT_DUPLICATEDBCOLUMN = 8;
00042 const MDFSTAT_FIELDDOESNOTEXIST = 16;
00043 const MDFSTAT_ILLEGALNAME = 32;
00044 const MDFSTAT_DUPLICATELABEL = 64;
00045 const MDFSTAT_ILLEGALLABEL = 128;
00046
00047 # object constructor
00048 function MetadataSchema()
00049 {
00050 # set up item factory base class
00051 $this->ItemFactory(
00052 "MetadataField", "MetadataFields", "FieldId", "FieldName");
00053
00054 # start with field info caching enabled
00055 $this->CachingOn = TRUE;
00056 }
00057
00058 # turn internal caching of field info on or off
00059 function CacheData($NewValue)
00060 {
00061 $this->CachingOn = $NewValue;
00062 }
00063
00064 # add new metadata field
00065 function AddField($FieldName, $FieldType, $Optional = TRUE, $DefaultValue = NULL)
00066 {
00067 # create new field
00068 $Field = new MetadataField(NULL, $FieldName, $FieldType, $Optional, $DefaultValue);
00069
00070 # save error code if create failed and return NULL
00071 if ($Field->Status() != MetadataSchema::MDFSTAT_OK)
00072 {
00073 $this->ErrorStatus = $Field->Status();
00074 $Field = NULL;
00075 }
00076
00077 # return new field to caller
00078 return $Field;
00079 }
00080
00090 function AddFieldFromXml($Xml)
00091 {
00092 # assume field addition will fail
00093 $Field = self::MDFSTAT_ERROR;
00094
00095 # add XML prefixes if needed
00096 $Xml = trim($Xml);
00097 if (!preg_match("/^<\?xml/i", $Xml))
00098 {
00099 if (!preg_match("/^<document>/i", $Xml))
00100 {
00101 $Xml = "<document>".$Xml."</document>";
00102 }
00103 $Xml = "<?xml version='1.0'?>".$Xml;
00104 }
00105
00106 # parse XML
00107 $XmlData = simplexml_load_string($Xml);
00108
00109 # if required values are present
00110 if (is_object($XmlData)
00111 && isset($XmlData->Name)
00112 && isset($XmlData->Type)
00113 && constant("MetadataSchema::".$XmlData->Type))
00114 {
00115 # create the metadata field
00116 $Field = new MetadataField(NULL, $XmlData->Name,
00117 constant("MetadataSchema::".$XmlData->Type));
00118
00119 # if field creation failed
00120 if ($Field->Status() !== self::MDFSTAT_OK)
00121 {
00122 # reset field value to error code
00123 $Field = $Field->Status();
00124 }
00125 else
00126 {
00127 # for other field attributes
00128 foreach ($XmlData as $MethodName => $Value)
00129 {
00130 # if they look valid and have not already been set
00131 if (method_exists($Field, $MethodName)
00132 && ($MethodName != "Name")
00133 && ($MethodName != "Type"))
00134 {
00135 # condense down any extraneous whitespace
00136 $Value = preg_replace("/\s+/", " ", trim($Value));
00137
00138 # set value for field
00139 $Field->$MethodName($Value);
00140 }
00141 }
00142
00143 # make new field permanent
00144 $Field->IsTempItem(FALSE);
00145 }
00146 }
00147
00148 # return new field (if any) to caller
00149 return $Field;
00150 }
00151
00152 # delete metadata field
00153 function DropField($FieldId)
00154 {
00155 $Field = new MetadataField($FieldId);
00156 $Field->Drop();
00157 }
00158
00159 # retrieve field by ID
00160 function GetField($FieldId)
00161 {
00162 static $Fields;
00163
00164 # if caching is off or field is already loaded
00165 if (($this->CachingOn != TRUE) || !isset($Fields[$FieldId]))
00166 {
00167 # retrieve field
00168 $Fields[$FieldId] = new MetadataField($FieldId);
00169 }
00170
00171 # return field to caller
00172 return $Fields[$FieldId];
00173 }
00174
00181 function GetFieldByName($FieldName, $IgnoreCase = FALSE)
00182 {
00183 $FieldId = $this->GetFieldIdByName($FieldName, $IgnoreCase);
00184 return ($FieldId === NULL) ? NULL : $this->GetField($FieldId);
00185 }
00186
00193 function GetFieldByLabel($FieldLabel, $IgnoreCase = FALSE)
00194 {
00195 $FieldId = $this->GetFieldIdByLabel($FieldLabel, $IgnoreCase);
00196 return ($FieldId === NULL) ? NULL : $this->GetField($FieldId);
00197 }
00198
00206 function GetFieldIdByName($FieldName, $IgnoreCase = FALSE)
00207 {
00208 static $FieldIdsByName;
00209
00210 # if caching is off or field ID is already loaded
00211 if (($this->CachingOn != TRUE) || !isset($FieldIdsByName[$FieldName]))
00212 {
00213 # retrieve field ID from DB
00214 $Condition = $IgnoreCase
00215 ? "WHERE LOWER(FieldName) = '".addslashes(strtolower($FieldName))."'"
00216 : "WHERE FieldName = '".addslashes($FieldName)."'";
00217 $FieldIdsByName[$FieldName] = $this->DB->Query(
00218 "SELECT FieldId FROM MetadataFields ".$Condition, "FieldId");
00219 }
00220
00221 return $FieldIdsByName[$FieldName];
00222 }
00223
00231 function GetFieldIdByLabel($FieldLabel, $IgnoreCase = FALSE)
00232 {
00233 static $FieldIdsByLabel;
00234
00235 # if caching is off or field ID is already loaded
00236 if (($this->CachingOn != TRUE) || !isset($FieldIdsByLabel[$FieldLabel]))
00237 {
00238 # retrieve field ID from DB
00239 $Condition = $IgnoreCase
00240 ? "WHERE LOWER(Label) = '".addslashes(strtolower($FieldLabel))."'"
00241 : "WHERE Label = '".addslashes($FieldLabel)."'";
00242 $FieldIdsByLabel[$FieldLabel] = $this->DB->Query(
00243 "SELECT FieldId FROM MetadataFields ".$Condition, "FieldId");
00244 }
00245
00246 return $FieldIdsByLabel[$FieldLabel];
00247 }
00248
00249 # check whether field with specified name exists
00250 function FieldExists($FieldName) { return $this->NameIsInUse($FieldName); }
00251
00252 # retrieve array of fields
00253 function GetFields($FieldTypes = NULL, $OrderType = NULL,
00254 $IncludeDisabledFields = FALSE, $IncludeTempFields = FALSE)
00255 {
00256 # create empty array to pass back
00257 $Fields = array();
00258
00259 # for each field type in database
00260 if ($IncludeTempFields && $IncludeDisabledFields)
00261 {
00262 $this->DB->Query("SELECT FieldId, FieldType FROM MetadataFields");
00263 }
00264 else
00265 {
00266 if ($IncludeTempFields)
00267 {
00268 $this->DB->Query("SELECT FieldId, FieldType FROM MetadataFields WHERE Enabled != 0");
00269 }
00270 elseif ($IncludeDisabledFields)
00271 {
00272 $this->DB->Query("SELECT FieldId, FieldType FROM MetadataFields WHERE FieldId >= 0");
00273 }
00274 else
00275 {
00276 $this->DB->Query("SELECT FieldId, FieldType FROM MetadataFields WHERE FieldId >= 0 AND Enabled != 0");
00277 }
00278 }
00279 while ($Record = $this->DB->FetchRow())
00280 {
00281 # if no specific type requested or if field is of requested type
00282 if (($FieldTypes == NULL)
00283 || (MetadataField::$FieldTypePHPEnums[$Record["FieldType"]] & $FieldTypes))
00284 {
00285 # create field object and add to array to be passed back
00286 $Fields[$Record["FieldId"]] = $this->GetField($Record["FieldId"]);
00287 }
00288 }
00289
00290 # if field sorting requested
00291 if ($OrderType !== NULL)
00292 {
00293 # sort field array by requested order type
00294 $this->FieldCompareType = $OrderType;
00295 $this->FieldOrderError = FALSE;
00296 uasort($Fields, array($this, "CompareFieldOrder"));
00297
00298 # if field order error detected
00299 if ($this->FieldOrderError)
00300 {
00301 # repair (reset) field order
00302 $OrderIndex = 1;
00303 foreach ($Fields as $Field)
00304 {
00305 $Field->OrderPosition($OrderType, $OrderIndex);
00306 $OrderIndex++;
00307 }
00308 }
00309 }
00310
00311 # return array of field objects to caller
00312 return $Fields;
00313 }
00314
00315 # callback function for sorting fields
00316 function CompareFieldOrder($FieldA, $FieldB)
00317 {
00318 if ($this->FieldCompareType == MetadataSchema::MDFORDER_ALPHABETICAL)
00319 {
00320 return ($FieldA->GetDisplayName() < $FieldB->GetDisplayName()) ? -1 : 1;
00321 }
00322 else
00323 {
00324 if ($FieldA->OrderPosition($this->FieldCompareType)
00325 == $FieldB->OrderPosition($this->FieldCompareType))
00326 {
00327 $this->FieldOrderError = TRUE;
00328 return 0;
00329 }
00330 else
00331 {
00332 return ($FieldA->OrderPosition($this->FieldCompareType)
00333 < $FieldB->OrderPosition($this->FieldCompareType)) ? -1 : 1;
00334 }
00335 }
00336 }
00337
00338 function GetFieldNames($FieldTypes = NULL, $OrderType = NULL,
00339 $IncludeDisabledFields = FALSE, $IncludeTempFields = FALSE)
00340 {
00341 global $DB;
00342
00343 $FieldNames=array();
00344 $Fields = $this->GetFields($FieldTypes, $OrderType, $IncludeDisabledFields, $IncludeTempFields);
00345
00346 foreach($Fields as $Field)
00347 {
00348 $DB->Query("SELECT FieldName FROM MetadataFields WHERE FieldId=".$Field->Id());
00349 $FieldNames[ $Field->Id() ] = $DB->FetchField("FieldName");
00350 }
00351
00352 return $FieldNames;
00353 }
00354
00370 function GetFieldsAsOptionList($OptionListName, $FieldTypes = NULL,
00371 $SelectedFieldId = NULL, $IncludeNullOption = TRUE,
00372 $AddEntries = NULL, $AllowMultiple = FALSE)
00373 {
00374 # retrieve requested fields
00375 $FieldNames = $this->GetFieldNames($FieldTypes);
00376
00377 # transform field names to labels
00378 foreach ($FieldNames as $FieldId => $FieldName)
00379 {
00380 $FieldNames[$FieldId] = $this->GetField($FieldId)->GetDisplayName();
00381 }
00382
00383 # begin HTML option list
00384 $Html = "<select id=\"".$OptionListName."\" name=\"".$OptionListName."\"";
00385
00386 # if multiple selections should be allowed
00387 if ($AllowMultiple)
00388 {
00389 $Html .= " multiple=\"multiple\"";
00390 }
00391
00392 $Html .= ">\n";
00393
00394 if ($IncludeNullOption)
00395 {
00396 $Html .= "<option value=\"\">--</option>\n";
00397 }
00398
00399 # make checking for IDs simpler
00400 if (!is_array($SelectedFieldId))
00401 {
00402 $SelectedFieldId = array($SelectedFieldId);
00403 }
00404
00405 # for each metadata field
00406 foreach ($FieldNames as $Id => $Name)
00407 {
00408 # add entry for field to option list
00409 $Html .= "<option value=\"".$Id."\"";
00410 if (in_array($Id, $SelectedFieldId)) { $Html .= " selected"; }
00411 $Html .= ">".htmlspecialchars($Name)."</option>\n";
00412 }
00413
00414 # if additional entries were requested
00415 if ($AddEntries)
00416 {
00417 foreach ($AddEntries as $Value => $Label)
00418 {
00419 $Html .= "<option value=\"".$Value."\"";
00420 if (in_array($Value,$SelectedFieldId)) { $Html .= " selected"; }
00421 $Html .= ">".htmlspecialchars($Label)."</option>\n";
00422 }
00423 }
00424
00425 # end HTML option list
00426 $Html .= "</select>\n";
00427
00428 # return constructed HTML to caller
00429 return $Html;
00430 }
00431
00432 # retrieve array of field types (enumerated type => field name)
00433 function GetFieldTypes()
00434 {
00435 return MetadataField::$FieldTypeDBEnums;
00436 }
00437
00438 # retrieve array of field types that user can create (enumerated type => field name)
00439 function GetAllowedFieldTypes()
00440 {
00441 return MetadataField::$FieldTypeDBAllowedEnums;
00442 }
00443
00444 # remove all metadata field associations for a given qualifier
00445 function RemoveQualifierAssociations($QualifierIdOrObject)
00446 {
00447 # sanitize qualifier ID or grab it from object
00448 $QualifierIdOrObject = is_object($QualifierIdOrObject)
00449 ? $QualifierIdOrObject->Id() : intval($QualifierIdOrObject);
00450
00451 # delete intersection records from database
00452 $this->DB->Query("DELETE FROM FieldQualifierInts WHERE QualifierId = "
00453 .$QualifierIdOrObject);
00454 }
00455
00456 # return whether qualifier is in use by metadata field
00457 function QualifierIsInUse($QualifierIdOrObject)
00458 {
00459 # sanitize qualifier ID or grab it from object
00460 $QualifierIdOrObject = is_object($QualifierIdOrObject)
00461 ? $QualifierIdOrObject->Id() : intval($QualifierIdOrObject);
00462
00463 # determine whether any fields use qualifier as default
00464 $DefaultCount = $this->DB->Query("SELECT COUNT(*) AS RecordCount FROM MetadataFields"
00465 ." WHERE DefaultQualifier = ".$QualifierIdOrObject,
00466 "RecordCount");
00467
00468 # determine whether any fields are associated with qualifier
00469 $AssociationCount = $this->DB->Query("SELECT COUNT(*) AS RecordCount FROM FieldQualifierInts"
00470 ." WHERE QualifierId = ".$QualifierIdOrObject,
00471 "RecordCount");
00472
00473 # report whether qualifier is in use based on defaults and associations
00474 return (($DefaultCount + $AssociationCount) > 0) ? TRUE : FALSE;
00475 }
00476
00477 # move fields up or down in field order
00478 function MoveUpInOrder($FieldIdOrObj, $OrderType)
00479 {
00480 $this->MoveFieldInOrder($FieldIdOrObj, $OrderType, FALSE);
00481 }
00482 function MoveDownInOrder($FieldIdOrObj, $OrderType)
00483 {
00484 $this->MoveFieldInOrder($FieldIdOrObj, $OrderType, TRUE);
00485 }
00486
00487 # return highest field ID currently in use
00488 function GetHighestFieldId() { return $this->GetHighestItemId(); }
00489
00497 static function StdNameToFieldMapping($MappedName, $FieldId = NULL)
00498 {
00499 if ($FieldId !== NULL)
00500 {
00501 self::$FieldMappings[$MappedName] = $FieldId;
00502 }
00503 return isset(self::$FieldMappings[$MappedName])
00504 ? self::$FieldMappings[$MappedName] : NULL;
00505 }
00506
00513 static function FieldToStdNameMapping($FieldId)
00514 {
00515 if ($FieldId != -1)
00516 {
00517 foreach (self::$FieldMappings as $MappedName => $MappedFieldId)
00518 {
00519 if ($MappedFieldId == $FieldId)
00520 {
00521 return $MappedName;
00522 }
00523 }
00524 }
00525 return NULL;
00526 }
00527
00535 function GetFieldByMappedName($MappedName)
00536 {
00537 return ($this->StdNameToFieldMapping($MappedName) == NULL) ? NULL
00538 : $this->GetField($this->StdNameToFieldMapping($MappedName));
00539 }
00540
00545 public function GetOwnedFields()
00546 {
00547 $Fields = array();
00548
00549 $this->DB->Query("
00550 SELECT * FROM MetadataFields
00551 WHERE Owner IS NOT NULL AND LENGTH(Owner) > 0");
00552
00553 while (FALSE !== ($Row = $this->DB->FetchRow()))
00554 {
00555 $FieldId = $Row["FieldId"];
00556 $Fields[$FieldId] = $this->GetField($FieldId);
00557 }
00558
00559 return $Fields;
00560 }
00561
00567 public static function SetOwnerListRetrievalFunction($Callback)
00568 {
00569 if (is_callable($Callback))
00570 {
00571 self::$OwnerListRetrievalFunction = $Callback;
00572 }
00573 }
00574
00580 public static function NormalizeOwnedFields()
00581 {
00582 # if an owner list retrieval function exists
00583 if (self::$OwnerListRetrievalFunction)
00584 {
00585 # retrieve the list of owners that currently exist
00586 $OwnerList = call_user_func(self::$OwnerListRetrievalFunction);
00587
00588 # an array is expected
00589 if (is_array($OwnerList))
00590 {
00591 $Schema = new MetadataSchema();
00592
00593 # get each metadata field that is owned by a plugin
00594 $OwnedFields = $Schema->GetOwnedFields();
00595
00596 # loop through each owned field
00597 foreach ($OwnedFields as $OwnedField)
00598 {
00599 # the owner of the current field
00600 $Owner = $OwnedField->Owner();
00601
00602 # if the owner of the field is in the list of owners that
00603 # currently exist, i.e., available plugins
00604 if (in_array($Owner, $OwnerList))
00605 {
00606 # enable the field and reset its "enable on owner return"
00607 # flag if the "enable on owner return" flag is currently
00608 # set to true. in other words, re-enable the field since
00609 # the owner has returned to the list of existing owners
00610 if ($OwnedField->EnableOnOwnerReturn())
00611 {
00612 $OwnedField->Enabled(TRUE);
00613 $OwnedField->EnableOnOwnerReturn(FALSE);
00614 }
00615 }
00616
00617 # if the owner of the field is *not* in the list of owners
00618 # that currently exist, i.e., available plugins
00619 else
00620 {
00621 # first, see if the field is currently enabled since it
00622 # will determine whether the field is re-enabled when
00623 # the owner becomes available again
00624 $Enabled = $OwnedField->Enabled();
00625
00626 # if the field is enabled, set its "enable on owner
00627 # return" flag to true and disable the field. nothing
00628 # needs to be done if the field is already disabled
00629 if ($Enabled)
00630 {
00631 $OwnedField->EnableOnOwnerReturn($Enabled);
00632 $OwnedField->Enabled(FALSE);
00633 }
00634 }
00635 }
00636 }
00637 }
00638 }
00639
00640 # ---- PRIVATE INTERFACE -------------------------------------------------
00641
00642 private $FieldCompareType;
00643 private $FieldOrderError;
00644 private $CachingOn;
00645 private static $FieldMappings;
00646 protected static $OwnerListRetrievalFunction;
00647
00648 private function MoveFieldInOrder($FieldIdOrObj, $OrderType, $MoveFieldDown)
00649 {
00650 # grab field ID
00651 $FieldId = is_object($FieldIdOrObj) ? $Field->Id() : $FieldIdOrObj;
00652
00653 # retrieve array of fields
00654 $Fields = $this->GetFields(NULL, $OrderType);
00655
00656 # reverse array of fields if we are moving field down
00657 if ($MoveFieldDown)
00658 {
00659 $Fields = array_reverse($Fields);
00660 }
00661
00662 # for each field in order
00663 $PreviousField = NULL;
00664 foreach ($Fields as $Field)
00665 {
00666 # if field is the field to be moved
00667 if ($Field->Id() == $FieldId)
00668 {
00669 # if we have a previous field
00670 if ($PreviousField !== NULL)
00671 {
00672 # swap field with previous field according to order type
00673 $TempVal = $Field->OrderPosition($OrderType);
00674 $Field->OrderPosition($OrderType, $PreviousField->OrderPosition($OrderType));
00675 $PreviousField->OrderPosition($OrderType, $TempVal);
00676 }
00677 }
00678
00679 # save field for next iteration
00680 $PreviousField = $Field;
00681 }
00682 }
00683 }